diff --git src/js/_enqueues/admin/user-profile.js src/js/_enqueues/admin/user-profile.js
index df17620f48..25252bda18 100644
--- src/js/_enqueues/admin/user-profile.js
+++ src/js/_enqueues/admin/user-profile.js
@@ -91,6 +91,68 @@
 		});
 	}
 
+	/**
+	 * Handle the password reset button. Sets up an ajax callback to trigger sending
+	 * a password reset email.
+	 */
+	function bindPasswordRestLink() {
+		$( '#generate-reset-link' ).on( 'click', function() {
+			var $this  = $(this),
+				data = {
+					'user_id': userProfileL10n.user_id, // The user to send a reset to.
+					'nonce':   userProfileL10n.nonce    // Nonce to validate the action.
+				};
+
+				// Remove any previous error messages.
+				$this.parent().find( '.notice-error' ).remove();
+
+				// Send the reset request.
+				var resetAction =  wp.ajax.post( 'send-password-reset', data );
+
+				// Handle reset success.
+				resetAction.done( function( response ) {
+					addInlineNotice( $this, true, response );
+				} );
+
+				// Handle reset failure.
+				resetAction.fail( function( response ) {
+					addInlineNotice( $this, false, response );
+				} );
+
+		});
+
+	}
+
+	/**
+	 * Helper function to insert an inline notice of success or failure.
+	 *
+	 * @param {jQuery Object} $this   The button element: the message will be inserted
+	 *                                above this button
+	 * @param {bool}          success Whether the message is a success message.
+	 * @param {string}        message The message to insert.
+	 */
+	function addInlineNotice( $this, success, message ) {
+		var resultDiv = $( '<div />' );
+
+		// Set up the notice div.
+		resultDiv.addClass( 'notice inline' );
+
+		// Add a class indicating success or failure.
+		resultDiv.addClass( 'notice-' + ( success ? 'success' : 'error' ) );
+
+		// Add the message, wrapping in a p tag, with a fadein to highlight each message.
+		resultDiv.text( $( $.parseHTML( message ) ).text() ).wrapInner( '<p />');
+
+		// Disable the button when the callback has succeeded.
+		$this.prop( 'disabled', success );
+
+		// Remove any previous notices.
+		$this.siblings( '.notice' ).remove();
+
+		// Insert the notice.
+		$this.before( resultDiv );
+	}
+
 	function bindPasswordForm() {
 		var $generateButton,
 			$cancelButton;
@@ -369,6 +431,7 @@
 		});
 
 		bindPasswordForm();
+		bindPasswordRestLink();
 	});
 
 	$( '#destroy-sessions' ).on( 'click', function( e ) {
diff --git src/wp-admin/admin-ajax.php src/wp-admin/admin-ajax.php
index 144facf7fe..30e9a414b5 100644
--- src/wp-admin/admin-ajax.php
+++ src/wp-admin/admin-ajax.php
@@ -140,6 +140,7 @@ $core_actions_post = array(
 	'health-check-loopback-requests',
 	'health-check-get-sizes',
 	'toggle-auto-updates',
+	'send-password-reset',
 );
 
 // Deprecated.
diff --git src/wp-admin/includes/ajax-actions.php src/wp-admin/includes/ajax-actions.php
index 6ff2d2d9fd..1a9f1c65c6 100644
--- src/wp-admin/includes/ajax-actions.php
+++ src/wp-admin/includes/ajax-actions.php
@@ -5398,3 +5398,33 @@ function wp_ajax_toggle_auto_updates() {
 
 	wp_send_json_success();
 }
+
+/**
+ * Ajax handler sends a password reset link.
+ *
+ * @since 5.4.0
+ */
+function wp_ajax_send_password_reset() {
+
+	// Validate the nonce for this action.
+	$user_id = isset( $_POST['user_id'] ) ? (int) $_POST['user_id'] : 0;
+	check_ajax_referer( 'reset-password-for-' . $user_id, 'nonce' );
+
+	// Verify user capabilities.
+	if ( ! current_user_can( 'edit_user', $user_id ) ) {
+		wp_send_json_error( __( 'Cannot send password reset, permission denied.' ) );
+	}
+
+	// Send the password reset link.
+	$user    = get_userdata( $user_id );
+	$results = retrieve_password( $user->user_login );
+
+	if ( true === $results ) {
+		wp_send_json_success(
+			/* translators: 1: User's display name. */
+			sprintf( __( 'A password reset link was emailed to %s.' ), $user->display_name )
+		);
+	} else {
+		wp_send_json_error( $results );
+	}
+}
\ No newline at end of file
diff --git src/wp-admin/includes/class-wp-users-list-table.php src/wp-admin/includes/class-wp-users-list-table.php
index 7e1e79f500..dff630dc05 100644
--- src/wp-admin/includes/class-wp-users-list-table.php
+++ src/wp-admin/includes/class-wp-users-list-table.php
@@ -274,6 +274,11 @@ class WP_Users_List_Table extends WP_List_Table {
 			}
 		}
 
+		// Add a password reset link to the bulk actions dropdown.
+		if ( current_user_can( 'edit_users' ) ) {
+			$actions['resetpassword'] = __( 'Send password reset' );
+		}
+
 		return $actions;
 	}
 
@@ -450,7 +455,7 @@ class WP_Users_List_Table extends WP_List_Table {
 				$edit = "<strong>{$user_object->user_login}{$super_admin}</strong><br />";
 			}
 
-			if ( ! is_multisite() && get_current_user_id() != $user_object->ID && current_user_can( 'delete_user', $user_object->ID ) ) {
+			if ( ! is_multisite() && get_current_user_id() !== $user_object->ID && current_user_can( 'delete_user', $user_object->ID ) ) {
 				$actions['delete'] = "<a class='submitdelete' href='" . wp_nonce_url( "users.php?action=delete&amp;user=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Delete' ) . '</a>';
 			}
 			if ( is_multisite() && current_user_can( 'remove_user', $user_object->ID ) ) {
@@ -469,6 +474,11 @@ class WP_Users_List_Table extends WP_List_Table {
 				);
 			}
 
+			// Add a link to send the user a reset password link by email.
+			if ( get_current_user_id() !== $user_object->ID && current_user_can( 'edit_user', $user_object->ID ) ) {
+				$actions['resetpassword'] = "<a class='resetpassword' href='" . wp_nonce_url( "users.php?action=resetpassword&amp;users=$user_object->ID", 'bulk-users' ) . "'>" . __( 'Send password reset' ) . '</a>';
+			}
+
 			/**
 			 * Filters the action links displayed under each user in the Users list table.
 			 *
diff --git src/wp-admin/user-edit.php src/wp-admin/user-edit.php
index b09f14fac5..2c0c7b0a32 100644
--- src/wp-admin/user-edit.php
+++ src/wp-admin/user-edit.php
@@ -14,7 +14,7 @@ wp_reset_vars( array( 'action', 'user_id', 'wp_http_referer' ) );
 $user_id      = (int) $user_id;
 $current_user = wp_get_current_user();
 if ( ! defined( 'IS_PROFILE_PAGE' ) ) {
-	define( 'IS_PROFILE_PAGE', ( $user_id == $current_user->ID ) );
+	define( 'IS_PROFILE_PAGE', ( $user_id === $current_user->ID ) );
 }
 
 if ( ! $user_id && IS_PROFILE_PAGE ) {
@@ -91,7 +91,7 @@ $user_can_edit = current_user_can( 'edit_posts' ) || current_user_can( 'edit_pag
  */
 if ( is_multisite()
 	&& ! current_user_can( 'manage_network_users' )
-	&& $user_id != $current_user->ID
+	&& $user_id !== $current_user->ID
 	&& ! apply_filters( 'enable_edit_any_user_configuration', true )
 ) {
 	wp_die( __( 'Sorry, you are not allowed to edit this user.' ) );
@@ -164,7 +164,7 @@ switch ( $action ) {
 		$errors = edit_user( $user_id );
 
 		// Grant or revoke super admin status if requested.
-		if ( is_multisite() && is_network_admin() && ! IS_PROFILE_PAGE && current_user_can( 'manage_network_options' ) && ! isset( $super_admins ) && empty( $_POST['super_admin'] ) == is_super_admin( $user_id ) ) {
+		if ( is_multisite() && is_network_admin() && ! IS_PROFILE_PAGE && current_user_can( 'manage_network_options' ) && ! isset( $super_admins ) && empty( $_POST['super_admin'] ) === is_super_admin( $user_id ) ) {
 			empty( $_POST['super_admin'] ) ? revoke_super_admin( $user_id ) : grant_super_admin( $user_id );
 		}
 
@@ -506,7 +506,7 @@ endif;
 		<th><label for="email"><?php _e( 'Email' ); ?> <span class="description"><?php _e( '(required)' ); ?></span></label></th>
 		<td><input type="email" name="email" id="email" aria-describedby="email-description" value="<?php echo esc_attr( $profileuser->user_email ); ?>" class="regular-text ltr" />
 		<?php
-		if ( $profileuser->ID == $current_user->ID ) :
+		if ( $profileuser->ID === $current_user->ID ) :
 			?>
 		<p class="description" id="email-description">
 			<?php _e( 'If you change this, we will send you an email at your new address to confirm it. <strong>The new address will not become active until confirmed.</strong>' ); ?>
@@ -515,7 +515,7 @@ endif;
 		endif;
 
 		$new_email = get_user_meta( $current_user->ID, '_new_email', true );
-		if ( $new_email && $new_email['newemail'] != $current_user->user_email && $profileuser->ID == $current_user->ID ) :
+		if ( $new_email && $new_email['newemail'] !== $current_user->user_email && $profileuser->ID === $current_user->ID ) :
 			?>
 		<div class="updated inline">
 		<p>
@@ -609,6 +609,27 @@ endif;
 	</td>
 </tr>
 <?php endif; ?>
+		<?php
+		// Allow admins to send reset password link
+		if ( ! IS_PROFILE_PAGE ) :
+			?>
+	<tr class="user-sessions-wrap hide-if-no-js">
+		<th><?php _e( 'Password Reset' ); ?></th>
+		<td>
+			<div class="generate-reset-link">
+				<button type="button" class="button button-secondary" id="generate-reset-link">
+					<?php _e( 'Send Reset Link' ); ?>
+				</button>
+			</div>
+			<p class="description">
+				<?php
+				/* translators: 1: User's display name. */
+				printf( __( 'Send %s a link to reset their password. This will not change their password, nor will it force a change.' ), esc_html( $profileuser->display_name ) );
+				?>
+			</p>
+		</td>
+	</tr>
+		<?php endif; ?>
 
 		<?php
 		/**
@@ -827,7 +848,7 @@ endif;
 			$output = '';
 			foreach ( $profileuser->caps as $cap => $value ) {
 				if ( ! $wp_roles->is_role( $cap ) ) {
-					if ( '' != $output ) {
+					if ( '' !== $output ) {
 						$output .= ', ';
 					}
 
diff --git src/wp-admin/users.php src/wp-admin/users.php
index e8f006b9f4..95bfe8c75c 100644
--- src/wp-admin/users.php
+++ src/wp-admin/users.php
@@ -128,7 +128,7 @@ switch ( $wp_list_table->current_action() ) {
 			}
 
 			// The new role of the current user must also have the promote_users cap or be a multisite super admin.
-			if ( $id == $current_user->ID && ! $wp_roles->role_objects[ $role ]->has_cap( 'promote_users' )
+			if ( $id === $current_user->ID && ! $wp_roles->role_objects[ $role ]->has_cap( 'promote_users' )
 			&& ! ( is_multisite() && current_user_can( 'manage_network_users' ) ) ) {
 					$update = 'err_admin_role';
 					continue;
@@ -183,7 +183,7 @@ switch ( $wp_list_table->current_action() ) {
 				wp_die( __( 'Sorry, you are not allowed to delete that user.' ), 403 );
 			}
 
-			if ( $id == $current_user->ID ) {
+			if ( $id === $current_user->ID ) {
 				$update = 'err_admin_del';
 				continue;
 			}
@@ -208,6 +208,46 @@ switch ( $wp_list_table->current_action() ) {
 		wp_redirect( $redirect );
 		exit;
 
+	case 'resetpassword':
+		check_admin_referer( 'bulk-users' );
+		if ( ! current_user_can( 'edit_users' ) ) {
+			$errors = new WP_Error( 'edit_users', __( 'You can&#8217;t edit users.' ) );
+		}
+		if ( empty( $_REQUEST['users'] ) ) {
+			wp_redirect( $redirect );
+			exit();
+		}
+		$userids = array_map( 'intval', (array) $_REQUEST['users'] );
+
+		$reset_count = 0;
+
+		foreach ( $userids as $id ) {
+			if ( ! current_user_can( 'edit_user', $id ) ) {
+				wp_die( __( 'You can&#8217;t edit that user.' ) );
+			}
+
+			if ( $id === $current_user->ID ) {
+				$update = 'err_admin_reset';
+				continue;
+			}
+
+			// Send the password reset link.
+			$user = get_userdata( $id );
+			if ( retrieve_password( $user->user_login ) ) {
+				++$reset_count;
+			}
+		}
+
+		$redirect = add_query_arg(
+			array(
+				'reset_count' => $reset_count,
+				'update'      => 'resetpassword',
+			),
+			$redirect
+		);
+		wp_redirect( $redirect );
+		exit;
+
 	case 'delete':
 		if ( is_multisite() ) {
 			wp_die( __( 'User deletion is not allowed from this screen.' ), 400 );
@@ -248,9 +288,9 @@ switch ( $wp_list_table->current_action() ) {
 		$users_have_content = (bool) apply_filters( 'users_have_additional_content', false, $userids );
 
 		if ( $userids && ! $users_have_content ) {
-			if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
+			if ( $wpdb->get_var( "SELECT ID FROM {$wpdb->posts} WHERE post_author IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
 				$users_have_content = true;
-			} elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) {
+			} elseif ( $wpdb->get_var( "SELECT link_id FROM {$wpdb->links} WHERE link_owner IN( " . implode( ',', $userids ) . ' ) LIMIT 1' ) ) { // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
 				$users_have_content = true;
 			}
 		}
@@ -284,7 +324,7 @@ switch ( $wp_list_table->current_action() ) {
 		$go_delete = 0;
 		foreach ( $all_userids as $id ) {
 			$user = get_userdata( $id );
-			if ( $id == $current_user->ID ) {
+			if ( $id === $current_user->ID ) {
 				/* translators: 1: User ID, 2: User login. */
 				echo '<li>' . sprintf( __( 'ID #%1$s: %2$s <strong>The current user will not be deleted.</strong>' ), $id, $user->user_login ) . "</li>\n";
 			} else {
@@ -302,7 +342,7 @@ switch ( $wp_list_table->current_action() ) {
 				?>
 			<input type="hidden" name="delete_option" value="delete" />
 			<?php else : ?>
-				<?php if ( 1 == $go_delete ) : ?>
+				<?php if ( 1 === $go_delete ) : ?>
 			<fieldset><p><legend><?php _e( 'What should be done with content owned by this user?' ); ?></legend></p>
 		<?php else : ?>
 			<fieldset><p><legend><?php _e( 'What should be done with content owned by these users?' ); ?></legend></p>
@@ -478,7 +518,7 @@ switch ( $wp_list_table->current_action() ) {
 				case 'del':
 				case 'del_many':
 					$delete_count = isset( $_GET['delete_count'] ) ? (int) $_GET['delete_count'] : 0;
-					if ( 1 == $delete_count ) {
+					if ( 1 === $delete_count ) {
 						$message = __( 'User deleted.' );
 					} else {
 						/* translators: %s: Number of users. */
@@ -504,6 +544,16 @@ switch ( $wp_list_table->current_action() ) {
 						);
 					}
 
+					$messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
+					break;
+				case 'resetpassword':
+					$reset_count = isset( $_GET['reset_count'] ) ? (int) $_GET['reset_count'] : 0;
+					if ( 1 === $reset_count ) {
+						$message = __( 'Password reset link sent.' );
+					} else {
+						/* translators: %s: Number of users. */
+						$message = sprintf( __( 'Password reset links sent to %s users.' ), $reset_count );
+					}
 					$messages[] = '<div id="message" class="updated notice is-dismissible"><p>' . $message . '</p></div>';
 					break;
 				case 'promote':
diff --git src/wp-includes/functions.php src/wp-includes/functions.php
index 536f195b14..f3bc6119e7 100644
--- src/wp-includes/functions.php
+++ src/wp-includes/functions.php
@@ -7781,3 +7781,173 @@ function is_php_version_compatible( $required ) {
 function wp_fuzzy_number_match( $expected, $actual, $precision = 1 ) {
 	return abs( (float) $expected - (float) $actual ) <= $precision;
 }
+
+
+/**
+ * Handles sending password retrieval email to user.
+ *
+ * @since 2.5.0
+ * @since 5.7.0 Added `$user_login` parameter.
+ *
+ * Note: prior to 5.7.0 this function was in wp_login.php.
+ *
+ * @global wpdb         $wpdb       WordPress database abstraction object.
+ * @global PasswordHash $wp_hasher  Portable PHP password hashing framework.
+ * @param  string       $user_login Optional user_login, default null. Uses
+ *                      $_POST['user_login'] if $user_login not set.
+ *
+ * @return bool|WP_Error True: when finish. WP_Error on error
+ */
+function retrieve_password( $user_login = null ) {
+	$errors    = new WP_Error();
+	$user_data = false;
+
+
+	// Use the passed $user_login if available, otherwise use $_POST['user_login'].
+	if ( !$user_login && !empty( $_POST['user_login'] ) ) {
+		$user_login = $_POST['user_login'];
+	}
+
+	if ( empty( $user_login ) ) {
+			$errors->add('empty_username', __('<strong>ERROR</strong>: Enter a username or email address.'));
+	} elseif ( strpos( $user_login, '@' ) ) {
+			$user_data = get_user_by( 'email', sanitize_email( $user_login ) );
+		if ( empty( $user_data ) )
+			$errors->add('invalid_email', __('<strong>ERROR</strong>: There is no user registered with that email address.'));
+	} else {
+			$user_data = get_user_by('login', sanitize_user( $user_login ) );
+	}
+
+	/**
+	 * Filters the user data during a password reset request.
+	 *
+	 * Allows, for example, custom validation using data other than username or email address.
+	 *
+	 * @since 5.7.0
+	 *
+	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
+	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
+	 *                                 by using invalid credentials.
+	 */
+	$user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors );
+
+	/**
+	 * Fires before errors are returned from a password reset request.
+	 *
+	 * @since 2.1.0
+	 * @since 4.4.0 Added the `$errors` parameter.
+	 * @since 5.4.0 Added the `$user_data` parameter.
+	 *
+	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
+	 *                                 by using invalid credentials.
+	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
+	 */
+	do_action( 'lostpassword_post', $errors, $user_data );
+
+	/**
+	 * Filters the errors encountered on a password reset request.
+	 *
+	 * The filtered WP_Error object may, for example, contain errors for an invalid
+	 * username or email address. A WP_Error object should always be returned,
+	 * but may or may not contain errors.
+	 *
+	 * If any errors are present in $errors, this will abort the password reset request.
+	 *
+	 * @since 5.5.0
+	 *
+	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
+	 *                                 by using invalid credentials.
+	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
+	 */
+	$errors = apply_filters( 'lostpassword_errors', $errors, $user_data );
+
+	if ( $errors->has_errors() ) {
+		return $errors;
+	}
+
+	if ( ! $user_data ) {
+		$errors->add( 'invalidcombo', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
+		return $errors;
+	}
+
+	// Redefining user_login ensures we return the right case in the email.
+	$user_login = $user_data->user_login;
+	$user_email = $user_data->user_email;
+	$key        = get_password_reset_key( $user_data );
+
+	if ( is_wp_error( $key ) ) {
+		return $key;
+	}
+
+	if ( is_multisite() ) {
+		$site_name = get_network()->site_name;
+	} else {
+		/*
+		 * The blogname option is escaped with esc_html on the way into the database
+		 * in sanitize_option. We want to reverse this for the plain text arena of emails.
+		 */
+		$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
+	}
+
+	$message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
+	/* translators: %s: Site name. */
+	$message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n";
+	/* translators: %s: User login. */
+	$message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n";
+	$message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
+	$message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
+	$message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n";
+
+	$requester_ip = $_SERVER['REMOTE_ADDR'];
+	if ( $requester_ip ) {
+		$message .= sprintf(
+			/* translators: %s: IP address of password reset requester. */
+			__( 'This password reset request originated from the IP address %s.' ),
+			$requester_ip
+		) . "\r\n";
+	}
+
+	/* translators: Password reset notification email subject. %s: Site title. */
+	$title = sprintf( __( '[%s] Password Reset' ), $site_name );
+
+	/**
+	 * Filters the subject of the password reset email.
+	 *
+	 * @since 2.8.0
+	 * @since 4.4.0 Added the `$user_login` and `$user_data` parameters.
+	 *
+	 * @param string  $title      Email subject.
+	 * @param string  $user_login The username for the user.
+	 * @param WP_User $user_data  WP_User object.
+	 */
+	$title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data );
+
+	/**
+	 * Filters the message body of the password reset mail.
+	 *
+	 * If the filtered message is empty, the password reset email will not be sent.
+	 *
+	 * @since 2.8.0
+	 * @since 4.1.0 Added `$user_login` and `$user_data` parameters.
+	 *
+	 * @param string  $message    Email message.
+	 * @param string  $key        The activation key.
+	 * @param string  $user_login The username for the user.
+	 * @param WP_User $user_data  WP_User object.
+	 */
+	$message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data );
+
+	if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) {
+		$errors->add(
+			'retrieve_password_email_failure',
+			sprintf(
+				/* translators: %s: Documentation URL. */
+				__( '<strong>Error</strong>: The email could not be sent. Your site may not be correctly configured to send emails. <a href="%s">Get support for resetting your password</a>.' ),
+				esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) )
+			)
+		);
+		return $errors;
+	}
+
+	return true;
+}
diff --git src/wp-includes/script-loader.php src/wp-includes/script-loader.php
index 59fef34afa..9d9fe39825 100644
--- src/wp-includes/script-loader.php
+++ src/wp-includes/script-loader.php
@@ -1078,6 +1078,15 @@ function wp_default_scripts( $scripts ) {
 
 	$scripts->add( 'user-profile', "/wp-admin/js/user-profile$suffix.js", array( 'jquery', 'password-strength-meter', 'wp-util' ), false, 1 );
 	$scripts->set_translations( 'user-profile' );
+	$user_id = isset( $_GET['user_id'] ) ? (int) $_GET['user_id'] : 0;
+	did_action( 'init' ) && $scripts->localize(
+		'user-profile',
+		'userProfileL10n',
+		array(
+			'user_id'  => $user_id,
+			'nonce'    => wp_create_nonce( 'reset-password-for-' . $user_id ),
+		)
+	);
 
 	$scripts->add( 'language-chooser', "/wp-admin/js/language-chooser$suffix.js", array( 'jquery' ), false, 1 );
 
diff --git src/wp-login.php src/wp-login.php
index 75c65865d9..128e3b254c 100644
--- src/wp-login.php
+++ src/wp-login.php
@@ -360,163 +360,6 @@ function wp_login_viewport_meta() {
 	<?php
 }
 
-/**
- * Handles sending a password retrieval email to a user.
- *
- * @since 2.5.0
- *
- * @return true|WP_Error True when finished, WP_Error object on error.
- */
-function retrieve_password() {
-	$errors    = new WP_Error();
-	$user_data = false;
-
-	if ( empty( $_POST['user_login'] ) || ! is_string( $_POST['user_login'] ) ) {
-		$errors->add( 'empty_username', __( '<strong>Error</strong>: Please enter a username or email address.' ) );
-	} elseif ( strpos( $_POST['user_login'], '@' ) ) {
-		$user_data = get_user_by( 'email', trim( wp_unslash( $_POST['user_login'] ) ) );
-		if ( empty( $user_data ) ) {
-			$errors->add( 'invalid_email', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
-		}
-	} else {
-		$login     = trim( wp_unslash( $_POST['user_login'] ) );
-		$user_data = get_user_by( 'login', $login );
-	}
-
-	/**
-	 * Filters the user data during a password reset request.
-	 *
-	 * Allows, for example, custom validation using data other than username or email address.
-	 *
-	 * @since 5.7.0
-	 *
-	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
-	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
-	 *                                 by using invalid credentials.
-	 */
-	$user_data = apply_filters( 'lostpassword_user_data', $user_data, $errors );
-
-	/**
-	 * Fires before errors are returned from a password reset request.
-	 *
-	 * @since 2.1.0
-	 * @since 4.4.0 Added the `$errors` parameter.
-	 * @since 5.4.0 Added the `$user_data` parameter.
-	 *
-	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
-	 *                                 by using invalid credentials.
-	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
-	 */
-	do_action( 'lostpassword_post', $errors, $user_data );
-
-	/**
-	 * Filters the errors encountered on a password reset request.
-	 *
-	 * The filtered WP_Error object may, for example, contain errors for an invalid
-	 * username or email address. A WP_Error object should always be returned,
-	 * but may or may not contain errors.
-	 *
-	 * If any errors are present in $errors, this will abort the password reset request.
-	 *
-	 * @since 5.5.0
-	 *
-	 * @param WP_Error      $errors    A WP_Error object containing any errors generated
-	 *                                 by using invalid credentials.
-	 * @param WP_User|false $user_data WP_User object if found, false if the user does not exist.
-	 */
-	$errors = apply_filters( 'lostpassword_errors', $errors, $user_data );
-
-	if ( $errors->has_errors() ) {
-		return $errors;
-	}
-
-	if ( ! $user_data ) {
-		$errors->add( 'invalidcombo', __( '<strong>Error</strong>: There is no account with that username or email address.' ) );
-		return $errors;
-	}
-
-	// Redefining user_login ensures we return the right case in the email.
-	$user_login = $user_data->user_login;
-	$user_email = $user_data->user_email;
-	$key        = get_password_reset_key( $user_data );
-
-	if ( is_wp_error( $key ) ) {
-		return $key;
-	}
-
-	if ( is_multisite() ) {
-		$site_name = get_network()->site_name;
-	} else {
-		/*
-		 * The blogname option is escaped with esc_html on the way into the database
-		 * in sanitize_option. We want to reverse this for the plain text arena of emails.
-		 */
-		$site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
-	}
-
-	$message = __( 'Someone has requested a password reset for the following account:' ) . "\r\n\r\n";
-	/* translators: %s: Site name. */
-	$message .= sprintf( __( 'Site Name: %s' ), $site_name ) . "\r\n\r\n";
-	/* translators: %s: User login. */
-	$message .= sprintf( __( 'Username: %s' ), $user_login ) . "\r\n\r\n";
-	$message .= __( 'If this was a mistake, ignore this email and nothing will happen.' ) . "\r\n\r\n";
-	$message .= __( 'To reset your password, visit the following address:' ) . "\r\n\r\n";
-	$message .= network_site_url( "wp-login.php?action=rp&key=$key&login=" . rawurlencode( $user_login ), 'login' ) . "\r\n\r\n";
-
-	$requester_ip = $_SERVER['REMOTE_ADDR'];
-	if ( $requester_ip ) {
-		$message .= sprintf(
-			/* translators: %s: IP address of password reset requester. */
-			__( 'This password reset request originated from the IP address %s.' ),
-			$requester_ip
-		) . "\r\n";
-	}
-
-	/* translators: Password reset notification email subject. %s: Site title. */
-	$title = sprintf( __( '[%s] Password Reset' ), $site_name );
-
-	/**
-	 * Filters the subject of the password reset email.
-	 *
-	 * @since 2.8.0
-	 * @since 4.4.0 Added the `$user_login` and `$user_data` parameters.
-	 *
-	 * @param string  $title      Email subject.
-	 * @param string  $user_login The username for the user.
-	 * @param WP_User $user_data  WP_User object.
-	 */
-	$title = apply_filters( 'retrieve_password_title', $title, $user_login, $user_data );
-
-	/**
-	 * Filters the message body of the password reset mail.
-	 *
-	 * If the filtered message is empty, the password reset email will not be sent.
-	 *
-	 * @since 2.8.0
-	 * @since 4.1.0 Added `$user_login` and `$user_data` parameters.
-	 *
-	 * @param string  $message    Email message.
-	 * @param string  $key        The activation key.
-	 * @param string  $user_login The username for the user.
-	 * @param WP_User $user_data  WP_User object.
-	 */
-	$message = apply_filters( 'retrieve_password_message', $message, $key, $user_login, $user_data );
-
-	if ( $message && ! wp_mail( $user_email, wp_specialchars_decode( $title ), $message ) ) {
-		$errors->add(
-			'retrieve_password_email_failure',
-			sprintf(
-				/* translators: %s: Documentation URL. */
-				__( '<strong>Error</strong>: The email could not be sent. Your site may not be correctly configured to send emails. <a href="%s">Get support for resetting your password</a>.' ),
-				esc_url( __( 'https://wordpress.org/support/article/resetting-your-password/' ) )
-			)
-		);
-		return $errors;
-	}
-
-	return true;
-}
-
 //
 // Main.
 //
