Index: src/wp-admin/includes/class-plugin-upgrader.php
===================================================================
--- src/wp-admin/includes/class-plugin-upgrader.php	(revision 48336)
+++ src/wp-admin/includes/class-plugin-upgrader.php	(working copy)
@@ -205,6 +205,14 @@
 		// Force refresh of plugin update information.
 		wp_clean_plugins_cache( $parsed_args['clear_update_cache'] );
 
+		// Ensure any new auto-update failures trigger a failure email when plugins update successfully.
+		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		if ( isset( $past_failure_emails[ $plugin ] ) ) {
+			unset( $past_failure_emails[ $plugin ] );
+			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
+		}
+
 		return true;
 	}
 
@@ -329,6 +337,19 @@
 		// Cleanup our hooks, in case something else does a upgrade on this connection.
 		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_plugin' ) );
 
+		// Ensure any new auto-update failures trigger a failure email when plugins update successfully.
+		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		foreach ( $results as $plugin => $result ) {
+			// Maintain last failure notification when plugins failed to update manually.
+			if ( ! isset( $past_failure_emails[ $plugin ] ) || ! $result || is_wp_error( $result ) ) {
+				continue;
+			}
+
+			unset( $past_failure_emails[ $plugin ] );
+		}
+		update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
+
 		return $results;
 	}
 
Index: src/wp-admin/includes/class-theme-upgrader.php
===================================================================
--- src/wp-admin/includes/class-theme-upgrader.php	(revision 48336)
+++ src/wp-admin/includes/class-theme-upgrader.php	(working copy)
@@ -313,6 +313,14 @@
 
 		wp_clean_themes_cache( $parsed_args['clear_update_cache'] );
 
+		// Ensure any new auto-update failures trigger a failure email when themes update successfully.
+		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		if ( isset( $past_failure_emails[ $theme ] ) ) {
+			unset( $past_failure_emails[ $theme ] );
+			update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
+		}
+
 		return true;
 	}
 
@@ -441,6 +449,19 @@
 		remove_filter( 'upgrader_post_install', array( $this, 'current_after' ) );
 		remove_filter( 'upgrader_clear_destination', array( $this, 'delete_old_theme' ) );
 
+		// Ensure any new auto-update failures trigger a failure email when themes update successfully.
+		$past_failure_emails = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		foreach ( $results as $theme => $result ) {
+			// Maintain last failure notification when themes failed to update manually.
+			if ( ! isset( $past_failure_emails[ $theme ] ) || ! $result || is_wp_error( $result ) ) {
+				continue;
+			}
+
+			unset( $past_failure_emails[ $theme ] );
+		}
+		update_option( 'auto_plugin_theme_update_emails', $past_failure_emails );
+
 		return $results;
 	}
 
Index: src/wp-admin/includes/class-wp-automatic-updater.php
===================================================================
--- src/wp-admin/includes/class-wp-automatic-updater.php	(revision 48336)
+++ src/wp-admin/includes/class-wp-automatic-updater.php	(working copy)
@@ -941,6 +941,44 @@
 			return;
 		}
 
+		$unique_failures = false;
+		$failure_emails  = get_option( 'auto_plugin_theme_update_emails', array() );
+
+		// When only failures have occurred, an email should only be sent if there are either new failures,
+		// or failures that have not sent out an email recently.
+		if ( 'fail' === $type ) {
+			$unique_failures = false;
+
+			/**
+			 * Filters the time interval between failure emails for a plugin.
+			 *
+			 * This prevents a site owner from receiving an email notice every time
+			 * the auto-update routine runs (every 12 hours by default).
+			 *
+			 * @param int $time Time interval in seconds between failure notifications.
+			 *                  Default is 259,200 (3 days).
+			 */
+			$failure_email_interval = apply_filters( 'auto_plugin_theme_update_email_fail_interval', DAY_IN_SECONDS * 3 );
+
+			foreach ( $failed_updates as $update_type => $failures ) {
+				foreach ( $failures as $failed_update ) {
+					if ( ! isset( $failure_emails[ $failed_update->item->{$update_type} ] ) ) {
+						$unique_failures = true;
+						continue;
+					}
+
+					// Enough time has passed to make this failure warrant an email.
+					if ( time() - $failure_emails[ $failed_update->item->{$update_type} ] > $failure_email_interval ) {
+						$unique_failures = true;
+					}
+				}
+			}
+
+			if ( ! $unique_failures ) {
+				return;
+			}
+		}
+
 		$body               = array();
 		$successful_plugins = ( ! empty( $successful_updates['plugin'] ) );
 		$successful_themes  = ( ! empty( $successful_updates['theme'] ) );
@@ -1017,7 +1055,8 @@
 				$body[] = __( 'These plugins failed to update:' );
 
 				foreach ( $failed_updates['plugin'] as $item ) {
-					$body[] = "- {$item->name}";
+					$body[]                                = "- {$item->name}";
+					$failure_emails[ $item->item->plugin ] = time();
 				}
 				$body[] = "\n";
 			}
@@ -1027,7 +1066,8 @@
 				$body[] = __( 'These themes failed to update:' );
 
 				foreach ( $failed_updates['theme'] as $item ) {
-					$body[] = "- {$item->name}";
+					$body[]                               = "- {$item->name}";
+					$failure_emails[ $item->item->theme ] = time();
 				}
 				$body[] = "\n";
 			}
@@ -1043,6 +1083,7 @@
 
 				foreach ( $successful_updates['plugin'] as $item ) {
 					$body[] = "- {$item->name}";
+					unset( $failure_emails[ $item->item->plugin ] );
 				}
 				$body[] = "\n";
 			}
@@ -1053,6 +1094,7 @@
 				// List successful updates.
 				foreach ( $successful_updates['theme'] as $item ) {
 					$body[] = "- {$item->name}";
+					unset( $failure_emails[ $item->item->theme ] );
 				}
 				$body[] = "\n";
 			}
@@ -1108,7 +1150,11 @@
 		 */
 		$email = apply_filters( 'auto_plugin_theme_update_email', $email, $type, $successful_updates, $failed_updates );
 
-		wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
+		$result = wp_mail( $email['to'], wp_specialchars_decode( $email['subject'] ), $email['body'], $email['headers'] );
+
+		if ( $result ) {
+			update_option( 'auto_plugin_theme_update_emails', $failure_emails );
+		}
 	}
 
 	/**
Index: src/wp-admin/includes/schema.php
===================================================================
--- src/wp-admin/includes/schema.php	(revision 48336)
+++ src/wp-admin/includes/schema.php	(working copy)
@@ -534,6 +534,7 @@
 		// 5.5.0
 		'blocklist_keys'                  => '',
 		'comment_previously_approved'     => 1,
+		'auto_plugin_theme_update_emails' => array(),
 	);
 
 	// 3.3.0
@@ -552,7 +553,13 @@
 	$options = wp_parse_args( $options, $defaults );
 
 	// Set autoload to no for these options.
-	$fat_options = array( 'moderation_keys', 'recently_edited', 'blocklist_keys', 'uninstall_plugins' );
+	$fat_options = array(
+		'moderation_keys',
+		'recently_edited',
+		'blocklist_keys',
+		'uninstall_plugins',
+		'auto_plugin_theme_update_emails',
+	);
 
 	$keys             = "'" . implode( "', '", array_keys( $options ) ) . "'";
 	$existing_options = $wpdb->get_col( "SELECT option_name FROM $wpdb->options WHERE option_name in ( $keys )" ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
Index: src/wp-includes/version.php
===================================================================
--- src/wp-includes/version.php	(revision 48336)
+++ src/wp-includes/version.php	(working copy)
@@ -20,7 +20,7 @@
  *
  * @global int $wp_db_version
  */
-$wp_db_version = 48121;
+$wp_db_version = 48337;
 
 /**
  * Holds the TinyMCE version.
