diff --git a/core/modules/user/config/install/user.mail.yml b/core/modules/user/config/install/user.mail.yml
index 382de7a59c0..93e744e0d30 100644
--- a/core/modules/user/config/install/user.mail.yml
+++ b/core/modules/user/config/install/user.mail.yml
@@ -14,6 +14,28 @@ cancel_confirm:
 
     This link expires in one day and nothing will happen if it is not used.
 
+    --  [site:name] team
+mail_change_notification:
+  subject: 'Email change for [user:display-name] at [site:name]'
+  body: |-
+    [user:display-name],
+
+    A request to change your email address has been made at [site:name]. In order to complete the change you will need to follow the instructions sent to your new email address within 24 hours.
+
+    If you did not intend to make this change, contact [site:mail].
+
+    --  [site:name] team
+mail_change_verification:
+  subject: 'Email change for [user:display-name] at [site:name]'
+  body: |-
+    [user:display-name],
+
+    A request to change your email address has been made at [site:name]. You need to verify the change by clicking on the link below or copying and pasting it in your browser:
+
+    [user:mail-change-url]
+
+    This link can only be used once and it expires after 24 hours. If not used, your email address at [site:name] will not change.
+
     --  [site:name] team
 password_reset:
   subject: 'Replacement login information for [user:display-name] at [site:name]'
diff --git a/core/modules/user/config/install/user.settings.yml b/core/modules/user/config/install/user.settings.yml
index 3232949963b..6b299969a02 100644
--- a/core/modules/user/config/install/user.settings.yml
+++ b/core/modules/user/config/install/user.settings.yml
@@ -3,6 +3,8 @@ anonymous: Anonymous
 verify_mail: true
 notify:
   cancel_confirm: true
+  mail_change_notification: true
+  mail_change_verification: true
   password_reset: true
   status_activated: true
   status_blocked: false
@@ -12,5 +14,6 @@ notify:
   register_pending_approval: true
 register: visitors
 cancel_method: user_cancel_block
+mail_change_timeout: 86400
 password_reset_timeout: 86400
 password_strength: true
diff --git a/core/modules/user/config/schema/user.schema.yml b/core/modules/user/config/schema/user.schema.yml
index ac54b6986d7..b056afb60b3 100644
--- a/core/modules/user/config/schema/user.schema.yml
+++ b/core/modules/user/config/schema/user.schema.yml
@@ -19,6 +19,12 @@ user.settings:
         cancel_confirm:
           type: boolean
           label: 'Account cancellation confirmation'
+        mail_change_notification:
+          type: boolean
+          label: 'Notify user when email changes'
+        mail_change_verification:
+          type: boolean
+          label: 'Require email verification when a user changes their email address'
         password_reset:
           type: boolean
           label: 'Notify user when password reset'
@@ -57,6 +63,9 @@ user.settings:
       label: 'When cancelling a user account'
       constraints:
         UserCancelMethod: []
+    mail_change_timeout:
+      type: integer
+      label: 'Mail change timeout'
     password_reset_timeout:
       type: integer
       label: 'Password reset timeout'
@@ -77,6 +86,12 @@ user.mail:
     cancel_confirm:
       type: mail
       label: 'Account cancellation confirmation'
+    mail_change_notification:
+      type: mail
+      label: 'Change mail notification'
+    mail_change_verification:
+      type: mail
+      label: 'Change mail verification'
     password_reset:
       type: mail
       label: 'Password recovery'
diff --git a/core/modules/user/src/AccountForm.php b/core/modules/user/src/AccountForm.php
index d0c5e8d2d9b..49b8f5acb73 100644
--- a/core/modules/user/src/AccountForm.php
+++ b/core/modules/user/src/AccountForm.php
@@ -410,6 +410,15 @@ protected function getEditedFieldNames(FormStateInterface $form_state) {
     ], parent::getEditedFieldNames($form_state));
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function actions(array $form, FormStateInterface $form_state) {
+    $actions = parent::actions($form, $form_state);
+    $actions['submit']['#submit'][] = '::notify';
+    return $actions;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -434,16 +443,67 @@ protected function flagViolations(EntityConstraintViolationListInterface $violat
     parent::flagViolations($violations, $form, $form_state);
   }
 
+
+  /**
+   * Sends notifications after the user account is updated.
+   *
+   * This callback is called after ::submitForm and ::save. It saves any new
+   * password hashes to the user entity. This ensures that notifications
+   * containing URLs generated by user_pass_rehash are valid.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   */
+  public function notify(array &$form, FormStateInterface $form_state): void {
+    $new_mail = $form_state->get('mail_change_verification');
+    if ($new_mail === NULL) {
+      return;
+    }
+    // Send a verification to the new email address.
+    /** @var \Drupal\user\UserInterface $account */
+    $account = $this->getEntity();
+    if (_user_mail_notify('mail_change_verification', $account, $new_mail) == NULL) {
+      // Make the change immediately if no verification email is configured.
+      $account->setEmail($new_mail);
+      $account->save();
+      return;
+    }
+    // Send notification email to the old email address, if it's set.
+    if ($account->getEmail()) {
+      _user_mail_notify('mail_change_notification', $account, NULL);
+    }
+    $this->messenger()
+      ->addWarning($this->t('You must confirm your email address. Further instructions have been sent to your new email address.'));
+  }
+
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+
+    /** @var \Drupal\user\UserInterface $account */
+    $account = $this->getEntity();
+    $new_mail = $form_state->getValue('mail');
+    $old_mail = $account->getEmail();
+
+    $own_account = (int) $this->currentUser()->id() === (int) $account->id();
+    $skip_verification = !$own_account || $this->currentUser()->hasPermission('administer users');
+    // Determine if this is a request to change the user email.
+    if (!$account->isNew() && ($old_mail !== $new_mail) && !$skip_verification) {
+      // Mark this as an email change so that after ::save, the password hashes
+      // are updated and verification emails are sent in ::notify.
+      $form_state->set('mail_change_verification', $new_mail);
+      $form_state->setValue('mail', $old_mail);
+    }
+
     parent::submitForm($form, $form_state);
 
-    $user = $this->getEntity();
     // If there's a session set to the users id, remove the password reset tag
     // since a new password was saved.
-    $this->getRequest()->getSession()->remove('pass_reset_' . $user->id());
+
+    $this->getRequest()->getSession()->remove('pass_reset_' . $account->id());
   }
 
 }
diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php
index 60ad0a6c917..39e35e743bf 100644
--- a/core/modules/user/src/AccountSettingsForm.php
+++ b/core/modules/user/src/AccountSettingsForm.php
@@ -174,7 +174,7 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     ];
     // These email tokens are shared for all settings, so just define
     // the list once to help ensure they stay in sync.
-    $email_token_help = $this->t('Available variables are: [site:name], [site:url], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:cancel-url].');
+    $email_token_help = $this->t('Available variables are: [site:name], [site:url], [site:mail], [user:display-name], [user:account-name], [user:mail], [site:login-url], [site:url-brief], [user:edit-url], [user:one-time-login-url], [user:mail-change-url], [user:cancel-url].');
 
     $form['email_admin_created'] = [
       '#type' => 'details',
@@ -281,6 +281,77 @@ public function buildForm(array $form, FormStateInterface $form_state) {
       '#rows' => 12,
     ];
 
+    $form['mail_change'] = [
+      '#type' => 'details',
+      '#title' => $this->t('Account email changing'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#group' => 'email',
+      '#weight' => 11,
+    ];
+    $form['mail_change']['mail_change_notification'] = [
+      '#type' => 'details',
+      '#tree' => TRUE,
+      '#title' => $this->t('Notification of old email'),
+      '#description' => $this->t("Edit the email message sent to the user's old email address when the email address is changed.") . ' ' . $email_token_help,
+      '#open' => TRUE,
+    ];
+    $form['mail_change']['mail_change_notification']['enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Notify user when email changes'),
+      '#config_target' => 'user.settings:notify.mail_change_notification',
+    ];
+    $states = [
+      'invisible' => [
+        'input[name="mail_change_notification[enabled]"]' => ['checked' => FALSE],
+      ],
+    ];
+    $form['mail_change']['mail_change_notification']['subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Subject'),
+      '#config_target' => 'user.mail:mail_change_notification.subject',
+      '#maxlength' => 180,
+      '#states' => $states,
+    ];
+    $form['mail_change']['mail_change_notification']['body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Body'),
+      '#config_target' => 'user.mail:mail_change_notification.body',
+      '#rows' => 12,
+      '#states' => $states,
+    ];
+    $form['mail_change']['mail_change_verification'] = [
+      '#type' => 'details',
+      '#tree' => TRUE,
+      '#title' => $this->t('Verification of new email'),
+      '#description' => $this->t("Edit the email message sent to user's new email address when the email address is changed.") . ' ' . $email_token_help,
+      '#open' => TRUE,
+    ];
+    $form['mail_change']['mail_change_verification']['enabled'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Require email verification when a user changes their email address'),
+      '#config_target' => 'user.settings:notify.mail_change_verification',
+    ];
+    $states = [
+      'invisible' => [
+        'input[name="mail_change_verification[enabled]"]' => ['checked' => FALSE],
+      ],
+    ];
+    $form['mail_change']['mail_change_verification']['subject'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Subject'),
+      '#config_target' => 'user.mail:mail_change_verification.subject',
+      '#maxlength' => 180,
+      '#states' => $states,
+    ];
+    $form['mail_change']['mail_change_verification']['body'] = [
+      '#type' => 'textarea',
+      '#title' => $this->t('Body'),
+      '#config_target' => 'user.mail:mail_change_verification.body',
+      '#rows' => 12,
+      '#states' => $states,
+    ];
+
     $form['email_activated'] = [
       '#type' => 'details',
       '#title' => $this->t('Account activation'),
diff --git a/core/modules/user/src/Controller/MailChangeController.php b/core/modules/user/src/Controller/MailChangeController.php
new file mode 100644
index 00000000000..acdbfdbad7d
--- /dev/null
+++ b/core/modules/user/src/Controller/MailChangeController.php
@@ -0,0 +1,155 @@
+<?php
+
+namespace Drupal\user\Controller;
+
+use Drupal\Component\Datetime\TimeInterface;
+use Drupal\Component\Utility\Crypt;
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Access\AccessResultInterface;
+use Drupal\Core\Controller\ControllerBase;
+use Drupal\Core\Site\Settings;
+use Drupal\Core\Url;
+use Drupal\user\UserFloodControlInterface;
+use Drupal\user\UserInterface;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+
+/**
+ * Provides a controller for email change routes.
+ */
+class MailChangeController extends ControllerBase {
+
+  public function __construct(protected UserFloodControlInterface $flood, protected TimeInterface $time) {}
+
+  /**
+   * Returns the user mail change page.
+   *
+   * This controller must return a redirect response. This is to prevent
+   * disclosure of an email change link via a referrer header.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user account requesting an email change.
+   * @param string $new_mail_hash
+   *   Hash of the new email address.
+   * @param int $timestamp
+   *   The timestamp when the hash was created.
+   * @param string $hash
+   *   Unique hash.
+   *
+   * @return \Symfony\Component\HttpFoundation\RedirectResponse
+   *   An HTTP redirect response.
+   */
+  public function page(UserInterface $user, string $new_mail_hash, int $timestamp, string $hash): RedirectResponse {
+    $messenger = $this->messenger();
+    $flood_config = $this->config('user.flood');
+    if (!$this->flood->isAllowed('user.email_change_ip', $flood_config->get('ip_limit'), $flood_config->get('ip_window'))) {
+      $messenger->addError($this->t('Too many email change requests from your IP address. It is temporarily blocked. Try again later or contact the site administrator.'));
+      return $this->redirect('<front>');
+    }
+    $this->flood->register('user.email_change_ip', $flood_config->get('ip_window'));
+
+    $timeout = $this->config('user.settings')->get('mail_change_timeout');
+    /** @var \Drupal\Core\Session\AccountProxyInterface $current_user */
+    $current_user = $this->currentUser();
+    $request_time = $this->time->getRequestTime();
+
+    // Another user is authenticated.
+    if ($current_user->isAuthenticated() && ((int) $current_user->id() !== (int) $user->id())) {
+      $arguments = [
+        '%user' => $current_user->getAccountName(),
+        ':logout' => Url::fromRoute('user.logout')->toString(),
+      ];
+      $messenger->addError($this->t('You are currently logged in as %user, and are attempting to confirm an email address change for another account. <a href=":logout">Log out</a> and try using the link again.', $arguments));
+      return $this->redirect('<front>');
+    }
+
+    // The link has expired.
+    if ($request_time - $timestamp > $timeout) {
+      $messenger->addError($this->t('You have tried to use an email address change link that has expired. Visit your account and change your email again.'));
+      return $this->redirect('<front>');
+    }
+
+    // Register flood events based on the UID only, so they apply for any IP
+    // address. This allows them to be cleared on successful reset from any IP.
+    $identifier = $user->id();
+    if (!$this->flood->isAllowed('user.email_change_user', $flood_config->get('user_limit'), $flood_config->get('user_window'), $identifier)) {
+      return $this->redirect('<front>');
+    }
+    $this->flood->register('user.email_change_user', $flood_config->get('user_window'), $identifier);
+
+    // The link is valid.
+    $new_mail = \Drupal::service('user.data')->get('user', $user->id(), 'email_change:' . $new_mail_hash);
+    if ($timestamp <= $request_time && $timestamp >= $user->getLastLoginTime() && hash_equals($hash, user_pass_rehash($user, $timestamp, $new_mail))) {
+      // Save the new email and also refresh the last login time so that this
+      // email change link is expired.
+      $user->setEmail($new_mail)->setLastLoginTime($request_time)->save();
+      $arguments = ['%mail' => $new_mail];
+      $messenger->addStatus($this->t('Your email address has been changed to %mail.', $arguments));
+      $this->flood->clear('user.email_change_user', $user->id());
+      return $this->redirect('<front>');
+    }
+
+    // The link is not valid. The timestamp from the link may be in the future
+    // or the user registered a new login in the meantime or the hash is not
+    // valid.
+    $messenger->addError($this->t('You have tried to use an email address change link that has either been used or is no longer valid. Visit your account and change your email again.'));
+
+    return $this->redirect('<front>');
+  }
+
+  /**
+   * Checks access to the change email URL.
+   *
+   * @param \Drupal\user\UserInterface $user
+   *   The user account requesting an email change.
+   *
+   * @return \Drupal\Core\Access\AccessResultInterface
+   *   An access result.
+   */
+  public function access(UserInterface $user): AccessResultInterface {
+    return AccessResult::allowedIf($user->isActive())->addCacheableDependency($user);
+  }
+
+  /**
+   * Generates a unique URL for a one time email change confirmation.
+   *
+   * @param \Drupal\user\UserInterface $account
+   *   An object containing the user account.
+   * @param array $options
+   *   (optional) A keyed array of settings. Supported options are:
+   *   - langcode: A language code to be used when generating locale-sensitive
+   *     URLs. If not specified, the user's preferred language is used.
+   *   - new_mail: The new user email when in the process of changing the
+   *     account email address.
+   * @param int $timestamp
+   *   (optional) The timestamp to use for creating the hash. Defaults to the
+   *   current request time.
+   * @param string $hash
+   *   (optional) Unique hash. If not defined, the hash is computed based on the
+   *   account data, the options array and the timestamp.
+   *
+   * @return \Drupal\Core\Url
+   *   A unique URL that provides a one-time email change confirmation.
+   */
+  public static function getUrl(UserInterface $account, array $options = [], $timestamp = NULL, $hash = NULL): Url {
+    $timestamp = $timestamp ?: \Drupal::time()->getRequestTime();
+    $langcode = $options['langcode'] ?? $account->getPreferredLangcode();
+    $new_mail = $options['new_mail'] ?? '';
+    $hash = empty($hash) ? user_pass_rehash($account, $timestamp, $new_mail) : $hash;
+    $url_options = [
+      'absolute' => TRUE,
+      'language' => \Drupal::service('language_manager')->getLanguage($langcode),
+    ];
+
+    // Create a hash of the the new mail address and save in user.data.
+    $new_mail_hash = Crypt::hmacBase64($new_mail, \Drupal::service('private_key')->get() . Settings::getHashSalt());
+    \Drupal::service('user.data')->set('user', $account->id(), 'email_change:' . $new_mail_hash, $new_mail);
+
+    return Url::fromRoute('user.mail_change', [
+      'user' => $account->id(),
+      'timestamp' => $timestamp,
+      'new_mail_hash' => $new_mail_hash,
+      'hash' => $hash,
+    ], $url_options);
+  }
+
+}
diff --git a/core/modules/user/src/Hook/UserHooks.php b/core/modules/user/src/Hook/UserHooks.php
index 4ed9f071a22..b809622679b 100644
--- a/core/modules/user/src/Hook/UserHooks.php
+++ b/core/modules/user/src/Hook/UserHooks.php
@@ -264,9 +264,14 @@ public function mail($key, &$message, $params): void {
     $original_language = $language_manager->getConfigOverrideLanguage();
     $language_manager->setConfigOverrideLanguage($language);
     $mail_config = \Drupal::config('user.mail');
-    $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE];
+    $new_mail = $params['new_mail'] ?? NULL;
+    $token_options = ['langcode' => $langcode, 'callback' => 'user_mail_tokens', 'clear' => TRUE, 'new_mail' => $new_mail];
     $message['subject'] .= PlainTextOutput::renderFromHtml($token_service->replace($mail_config->get($key . '.subject'), $variables, $token_options));
     $message['body'][] = $token_service->replacePlain($mail_config->get($key . '.body'), $variables, $token_options);
+    // Send to the new email when this is a mail change verification.
+    if ($key === "mail_change_verification") {
+      $message['to'] = $params['new_mail'] ?? $message['to'];
+    }
     $language_manager->setConfigOverrideLanguage($original_language);
   }
 
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index b2bb0d9f833..b789e56ac4e 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -14,6 +14,7 @@
 use Drupal\Core\Session\AnonymousUserSession;
 use Drupal\Core\Site\Settings;
 use Drupal\Core\Url;
+use Drupal\user\Controller\MailChangeController;
 use Drupal\user\Entity\User;
 use Drupal\user\UserInterface;
 
@@ -250,14 +251,14 @@ function user_login_finalize(UserInterface $account): void {
  *   A unique URL that provides a one-time log in for the user, from which
  *   they can change their password.
  */
-function user_pass_reset_url($account, $options = []) {
+function user_pass_reset_url($account, $options = [], $new_mail = NULL) {
   $timestamp = \Drupal::time()->getCurrentTime();
   $langcode = $options['langcode'] ?? $account->getPreferredLangcode();
   return Url::fromRoute('user.reset',
     [
       'uid' => $account->id(),
       'timestamp' => $timestamp,
-      'hash' => user_pass_rehash($account, $timestamp),
+      'hash' => user_pass_rehash($account, $timestamp, $new_mail),
     ],
     [
       'absolute' => TRUE,
@@ -314,11 +315,11 @@ function user_cancel_url(UserInterface $account, $options = []) {
  * @return string
  *   A string that is safe for use in URLs and SQL statements.
  */
-function user_pass_rehash(UserInterface $account, $timestamp) {
+function user_pass_rehash(UserInterface $account, $timestamp, $mail = NULL) {
   $data = $timestamp;
   $data .= ':' . $account->getLastLoginTime();
   $data .= ':' . $account->id();
-  $data .= ':' . $account->getEmail();
+  $data .= ':' . $mail ?: $account->getEmail();
   return Crypt::hmacBase64($data, Settings::getHashSalt() . $account->getPassword());
 }
 
@@ -519,13 +520,18 @@ function user_cancel_methods(): array {
  *   properties:
  *   - login: The UNIX timestamp of the user's last login.
  *   - pass: The hashed account login password.
+ *   - email: The user's email address.
  * @param array $options
  *   A keyed array of settings and flags to control the token replacement
  *   process. See \Drupal\Core\Utility\Token::replace().
  */
 function user_mail_tokens(&$replacements, $data, $options): void {
   if (isset($data['user'])) {
-    $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options);
+    $new_mail = $options['new_mail'] ?? NULL;
+    $replacements['[user:one-time-login-url]'] = user_pass_reset_url($data['user'], $options, $new_mail);
+    if ($data['user']->getEmail()) {
+      $replacements['[user:mail-change-url]'] = MailChangeController::getUrl($data['user'], $options)->toString(TRUE)->getGeneratedUrl();
+    }
     $replacements['[user:cancel-url]'] = user_cancel_url($data['user'], $options);
   }
 }
@@ -627,6 +633,10 @@ function user_role_revoke_permissions($rid, array $permissions = []): void {
  *   - 'register_pending_approval': Welcome message, user pending admin
  *     approval.
  *   - 'password_reset': Password recovery request.
+ *   - 'mail_change_notification': Email change notification, sent to the old
+ *     email address.
+ *   - 'mail_change_verification': Email change verification, sent to the new
+ *     email address.
  *   - 'status_activated': Account activated.
  *   - 'status_blocked': Account blocked.
  *   - 'cancel_confirm': Account cancellation request.
@@ -634,17 +644,23 @@ function user_role_revoke_permissions($rid, array $permissions = []): void {
  * @param \Drupal\Core\Session\AccountInterface $account
  *   The user object of the account being notified. Must contain at
  *   least the fields 'uid', 'name', and 'mail'.
+ * @param string|null $new_mail
+ *   The new user email when in the process of changing the account email
+ *   address.
  *
  * @return array
- *   An array containing various information about the message.
- *   See \Drupal\Core\Mail\MailManagerInterface::mail() for details.
+ *   An array containing various information about the message, as they are
+ *   returned by \Drupal\Core\Mail\MailManagerInterface::mail(), or NULL if the
+ *   notifications for the required operation are disabled.
  *
  * @see user_mail_tokens()
+ * @see \Drupal\Core\Mail\MailManagerInterface::mail()
  */
-function _user_mail_notify($op, AccountInterface $account) {
+function _user_mail_notify($op, AccountInterface $account, string $new_mail = NULL) {
 
   if (\Drupal::config('user.settings')->get('notify.' . $op)) {
     $params['account'] = $account;
+    $params['new_mail'] = $new_mail;
     // Get the custom site notification email to use as the from email address
     // if it has been set.
     $site_mail = \Drupal::config('system.site')->get('mail_notification');
diff --git a/core/modules/user/user.post_update.php b/core/modules/user/user.post_update.php
index 30e22584489..f6322b4403a 100644
--- a/core/modules/user/user.post_update.php
+++ b/core/modules/user/user.post_update.php
@@ -16,3 +16,31 @@ function user_removed_post_updates(): array {
     'user_post_update_sort_permissions_again' => '11.0.0',
   ];
 }
+
+
+/**
+ * Update config for change mail notifications.
+ */
+function user_post_update_mail_change(): void {
+  $config_factory = \Drupal::service('config.factory');
+
+  $config_factory->getEditable('user.settings')
+    ->set('notify.mail_change_notification', FALSE)
+    ->set('notify.mail_change_verification', FALSE)
+    ->set('mail_change_timeout', 86400)
+    ->save();
+
+  $mail_change_notification = [
+    'body' => "[user:display-name],\n\nA request to change your email address has been made at [site:name]. In order to complete the change you will need to follow the instructions sent to your new email address within 24 hours.\n\nIf you did not intend to make this change, contact [site:mail].\n\n--  [site:name] team",
+    'subject' => 'Email change for [user:display-name] at [site:name]',
+  ];
+  $mail_change_verification = [
+    'body' => "[user:display-name],\n\nA request to change your email address has been made at [site:name]. You need to verify the change by clicking on the link below or copying and pasting it in your browser:\n\n[user:mail-change-url]\n\nThis link can only be used once and it expires after 24 hours. If not used, your email address at [site:name] will not change.\n\n--  [site:name] team",
+    'subject' => 'Email change for [user:display-name] at [site:name]',
+  ];
+
+  $config_factory->getEditable('user.mail')
+    ->set('mail_change_notification', $mail_change_notification)
+    ->set('mail_change_verification', $mail_change_verification)
+    ->save();
+}
\ No newline at end of file
diff --git a/core/modules/user/user.routing.yml b/core/modules/user/user.routing.yml
index 206d8c01a13..c0e14b445a8 100644
--- a/core/modules/user/user.routing.yml
+++ b/core/modules/user/user.routing.yml
@@ -252,3 +252,15 @@ user.well-known.change_password:
     _controller: '\Drupal\user\Controller\UserController::userEditPage'
   requirements:
     _user_is_logged_in: 'TRUE'
+
+user.mail_change:
+  path: '/user/mail-change/{user}/{new_mail_hash}/{timestamp}/{hash}'
+  defaults:
+    _controller: '\Drupal\user\Controller\MailChangeController::page'
+    _title: 'Change email address'
+  requirements:
+    _custom_access: '\Drupal\user\Controller\MailChangeController::access'
+    user: \d+
+    timestamp: \d+
+  options:
+    no_cache: TRUE
\ No newline at end of file
diff --git a/core/profiles/demo_umami/config/install/user.settings.yml b/core/profiles/demo_umami/config/install/user.settings.yml
index cdc311b3c7d..a70229188cb 100644
--- a/core/profiles/demo_umami/config/install/user.settings.yml
+++ b/core/profiles/demo_umami/config/install/user.settings.yml
@@ -3,6 +3,8 @@ anonymous: Anonymous
 verify_mail: true
 notify:
   cancel_confirm: true
+  mail_change_notification: true
+  mail_change_verification: true
   password_reset: true
   status_activated: true
   status_blocked: false
@@ -12,5 +14,6 @@ notify:
   register_pending_approval: true
 register: admin_only
 cancel_method: user_cancel_block
+mail_change_timeout: 86400
 password_reset_timeout: 86400
 password_strength: true
diff --git a/core/profiles/minimal/config/install/user.settings.yml b/core/profiles/minimal/config/install/user.settings.yml
index cdc311b3c7d..a70229188cb 100644
--- a/core/profiles/minimal/config/install/user.settings.yml
+++ b/core/profiles/minimal/config/install/user.settings.yml
@@ -3,6 +3,8 @@ anonymous: Anonymous
 verify_mail: true
 notify:
   cancel_confirm: true
+  mail_change_notification: true
+  mail_change_verification: true
   password_reset: true
   status_activated: true
   status_blocked: false
@@ -12,5 +14,6 @@ notify:
   register_pending_approval: true
 register: admin_only
 cancel_method: user_cancel_block
+mail_change_timeout: 86400
 password_reset_timeout: 86400
 password_strength: true
diff --git a/core/profiles/standard/config/install/user.settings.yml b/core/profiles/standard/config/install/user.settings.yml
index cdc311b3c7d..a70229188cb 100644
--- a/core/profiles/standard/config/install/user.settings.yml
+++ b/core/profiles/standard/config/install/user.settings.yml
@@ -3,6 +3,8 @@ anonymous: Anonymous
 verify_mail: true
 notify:
   cancel_confirm: true
+  mail_change_notification: true
+  mail_change_verification: true
   password_reset: true
   status_activated: true
   status_blocked: false
@@ -12,5 +14,6 @@ notify:
   register_pending_approval: true
 register: admin_only
 cancel_method: user_cancel_block
+mail_change_timeout: 86400
 password_reset_timeout: 86400
 password_strength: true
