Index: src/wp-includes/general-template.php
===================================================================
--- src/wp-includes/general-template.php	(revision 46551)
+++ src/wp-includes/general-template.php	(working copy)
@@ -2565,7 +2565,8 @@
 		return false;
 	}
 
-	$datetime = get_post_datetime( $post );
+	$source   = ( $gmt ) ? 'gmt' : 'local';
+	$datetime = get_post_datetime( $post, 'date', $source );
 
 	if ( false === $datetime ) {
 		return false;
@@ -2606,13 +2607,21 @@
  *
  * The object will be set to the timezone from WordPress settings.
  *
+ * For legacy reasons, this function allows to choose to instance from local or UTC time in database.
+ * Normally this should make no difference to the result. However, the values might get out of sync in database,
+ * typically because of timezone setting changes. The parameter ensures the ability to reproduce backwards
+ * compatible behaviors in such cases.
+ *
  * @since 5.3.0
  *
- * @param int|WP_Post $post  Optional. WP_Post object or ID. Default is global `$post` object.
- * @param string      $field Optional. Post field to use. Accepts 'date' or 'modified'.
+ * @param int|WP_Post $post   Optional. WP_Post object or ID. Default is global `$post` object.
+ * @param string      $field  Optional. Published or modified time to use from database. Accepts 'date' or 'modified'.
+ *                            Default 'date'.
+ * @param string      $source Optional. Local or UTC time to use from database. Accepts 'local' or 'gmt'.
+ *                            Default 'local'.
  * @return DateTimeImmutable|false Time object on success, false on failure.
  */
-function get_post_datetime( $post = null, $field = 'date' ) {
+function get_post_datetime( $post = null, $field = 'date', $source = 'local' ) {
 	$post = get_post( $post );
 
 	if ( ! $post ) {
@@ -2619,13 +2628,27 @@
 		return false;
 	}
 
-	$time = ( 'modified' === $field ) ? $post->post_modified : $post->post_date;
+	$wp_timezone = wp_timezone();
 
+	if ( 'gmt' === $source ) {
+		$time     = ( 'modified' === $field ) ? $post->post_modified_gmt : $post->post_date_gmt;
+		$timezone = new DateTimeZone( 'UTC' );
+	} else {
+		$time     = ( 'modified' === $field ) ? $post->post_modified : $post->post_date;
+		$timezone = $wp_timezone;
+	}
+
 	if ( empty( $time ) || '0000-00-00 00:00:00' === $time ) {
 		return false;
 	}
 
-	return date_create_immutable_from_format( 'Y-m-d H:i:s', $time, wp_timezone() );
+	$datetime = date_create_immutable_from_format( 'Y-m-d H:i:s', $time, $timezone );
+
+	if ( false === $datetime ) {
+		return false;
+	}
+
+	return $datetime->setTimezone( $wp_timezone );
 }
 
 /**
@@ -2637,7 +2660,8 @@
  * @since 5.3.0
  *
  * @param int|WP_Post $post  Optional. WP_Post object or ID. Default is global `$post` object.
- * @param string      $field Optional. Post field to use. Accepts 'date' or 'modified'.
+ * @param string      $field Optional. Published or modified time to use from database. Accepts 'date' or 'modified'.
+ *                           Default 'date'.
  * @return int|false Unix timestamp on success, false on failure.
  */
 function get_post_timestamp( $post = null, $field = 'date' ) {
@@ -2729,7 +2753,8 @@
 		return false;
 	}
 
-	$datetime = get_post_datetime( $post, 'modified' );
+	$source   = ( $gmt ) ? 'gmt' : 'local';
+	$datetime = get_post_datetime( $post, 'modified', $source );
 
 	if ( false === $datetime ) {
 		return false;
Index: tests/phpunit/tests/date/postTime.php
===================================================================
--- tests/phpunit/tests/date/postTime.php	(revision 46551)
+++ tests/phpunit/tests/date/postTime.php	(working copy)
@@ -62,4 +62,51 @@
 		$this->assertEquals( $rfc3339, get_post_modified_time( DATE_RFC3339, false, $post_id, true ) );
 		$this->assertEquals( $rfc3339_utc, get_post_modified_time( DATE_RFC3339, true, $post_id, true ) );
 	}
+
+	/**
+	 * @ticket 48384
+	 */
+	public function test_should_keep_utc_time_on_timezone_change_with_gmt_offset() {
+		// Set the timezone to UTC+0.
+		update_option( 'gmt_offset', 0 );
+
+		$datetime = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) );
+		$mysql    = $datetime->format( 'Y-m-d H:i:s' );
+		$rfc3339  = $datetime->format( DATE_RFC3339 );
+		$post_id  = self::factory()->post->create(
+			array(
+				'post_date'     => $mysql,
+				'post_modified' => $mysql,
+			)
+		);
+
+		// Change the timezone to UTC+2.
+		update_option( 'gmt_offset', 2 );
+
+		$this->assertEquals( $rfc3339, get_post_time( DATE_RFC3339, true, $post_id ) );
+		$this->assertEquals( $rfc3339, get_post_modified_time( DATE_RFC3339, true, $post_id ) );
+	}
+
+	/**
+	 * @ticket 48384
+	 */
+	public function test_should_keep_utc_time_on_timezone_change() {
+		$timezone = 'UTC';
+		update_option( 'timezone_string', $timezone );
+
+		$datetime = new DateTimeImmutable( 'now', new DateTimeZone( $timezone ) );
+		$mysql    = $datetime->format( 'Y-m-d H:i:s' );
+		$rfc3339  = $datetime->format( DATE_RFC3339 );
+		$post_id  = self::factory()->post->create(
+			array(
+				'post_date'     => $mysql,
+				'post_modified' => $mysql,
+			)
+		);
+
+		update_option( 'timezone_string', 'Europe/Kiev' );
+
+		$this->assertEquals( $rfc3339, get_post_time( DATE_RFC3339, true, $post_id ) );
+		$this->assertEquals( $rfc3339, get_post_modified_time( DATE_RFC3339, true, $post_id ) );
+	}
 }
