diff --git a/src/wp-includes/class-wp-meta-manager.php b/src/wp-includes/class-wp-meta-manager.php
new file mode 100644
index 0000000..b5f33d9
--- /dev/null
+++ b/src/wp-includes/class-wp-meta-manager.php
@@ -0,0 +1,284 @@
+<?php
+/**
+ * A registry for meta data.
+ *
+ * @package WordPress
+ * @since   4.6.0
+ */
+final class WP_Meta_Manager {
+
+	/**
+	 * Registered meta of the post meta type (including pages and CPTs).
+	 *
+	 * @since  4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $post = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the taxonomy meta type.
+	 *
+	 * @since  4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $taxonomy = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the user meta type.
+	 *
+	 * @since  4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $user = array(
+		'all' => array()
+	);
+
+	/**
+	 * Registered meta of the comment meta type.
+	 *
+	 * @since  4.6.0
+	 * @access private
+	 * @var array
+	 */
+	private $comment = array(
+		'all' => array()
+	);
+
+	/**
+	 * @since 4.6.0
+	 */
+	public function __construct() {
+
+		$post_types_without_meta = array( 'revision', 'nav_menu_item' );
+
+		foreach ( get_post_types( null, 'names' ) as $post_type ) {
+			if ( in_array( $post_type, $post_types_without_meta ) ) {
+				continue;
+			}
+
+			$this->post[ $post_type ] = array();
+		}
+
+		foreach ( get_taxonomies() as $taxonomy ) {
+			$this->taxonomy[ $taxonomy ] = array();
+		}
+
+		foreach ( array( 'comment', 'ping' ) as $comment_type ) {
+			$this->comment[ $comment_type ] = array();
+		}
+
+	}
+
+	/**
+	 * Register a meta key.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param array|object $query          {
+	 *                                     Array or object of meta data properties.
+	 *
+	 * @type string        $object_type    The meta type this key belongs to, e.g. `post`.
+	 * @type string        $object_subtype The meta subtype this key belongs to, e.g. `page`.
+	 * @type string        $key            The meta key.
+	 * @type array         $schema         The meta field's schema, according to JSON schema.
+	 * @type array         $public         Whether the meta key should be publicly queryable.
+	 * @return true|WP_Error
+	 */
+	public function register( $args ) {
+
+		$defaults = array(
+			'object_type'           => null,
+			'object_subtype'        => null,
+			'key'                   => null,
+			'schema'                => false,
+			'public'                => false,
+			'show_in_rest'          => false,
+			'sanitize_callback'     => null,
+			'old_sanitize_callback' => null,
+			'auth_callback'         => null,
+			'old_auth_callback'     => null,
+		);
+
+		if ( is_array( $args ) ) {
+			// Merge passed $args array with defaults
+			$args = array_merge( $defaults, $args );
+		} elseif ( ! is_object( $args ) ) {
+			// No $args passed, use defaults
+			$args = $defaults;
+		}
+
+		// Force object
+		if ( ! is_object( $args ) ) {
+			$args = (object) $args;
+		}
+
+		// @todo validate object sub types to only those objects that support custom object types?
+		if ( empty( $args->object_subtype ) ) {
+			$args->object_subtype = 'all';
+		}
+
+		// @todo validate object type as post, comment, user, term?
+
+		if ( empty( $args->object_type ) ) {
+			return new WP_Error( 'missing_object_type', __( 'The object type is required to register meta.' ) );
+		}
+
+		if ( ! property_exists( $this, $args->object_type ) ) {
+			return new WP_Error( 'object_type_does_not_exist', __( sprintf( 'The object type "%s" does not exist.', esc_html( $args['object_type'] ) ) ) );
+		}
+
+		// @todo This may not be necessary, what if post types are registered later?
+		if ( ! empty( $args->object_subtype ) && ! isset( $this->{$args->object_type}[ $args->object_subtype ] ) ) {
+			return new WP_Error( 'meta_subtype_does_not_exist', __( 'The meta subtype does not exist in the registry.' ) );
+		}
+
+		if ( empty( $args->key ) ) {
+			return new WP_Error( 'meta_key_required', __( 'The meta key is required.' ) );
+		}
+
+		if ( ! isset( $args->schema ) ) {
+			return new WP_Error( 'schema_required', __( 'The JSON schema for the meta key is required.' ) );
+		}
+
+		if ( ! isset( $args->public ) ) {
+			return new WP_Error( 'public_required', __( 'The public argument is required to register meta.' ) );
+		}
+
+		if ( empty( $args->object_subtype ) ) {
+			$args->object_subtype = 'all';
+		}
+
+		// If neither new or legacy `auth_callback` is provided, fallback to `is_protected_meta()`.
+		if ( empty( $args->auth_callback ) && empty( $args->old_auth_callback ) ) {
+			if ( is_protected_meta( $args->key, $args->object_type ) ) {
+				$args->auth_callback = '__return_false';
+			} else {
+				$args->auth_callback = '__return_true';
+			}
+		}
+
+		$this->setup_hooks( $args );
+
+		$this->{$args->object_type}[ $args->object_subtype ][ $args->key ] = $args;
+
+		return true;
+
+	}
+
+	/**
+	 * Setup hooks for sanitize and authentication checks
+	 *
+	 * @param object $args
+	 */
+	private function setup_hooks( $args ) {
+
+		$object_type    = $args->object_type;
+		$object_subtype = $args->object_subtype;
+		$meta_key       = $args->key;
+
+		// This legacy filter will fire for all subtypes of an object type.
+		if ( is_callable( $args->old_sanitize_callback ) ) {
+			add_filter( "sanitize_{$object_type}_meta_{$meta_key}", $args->old_sanitize_callback, 10, 3 );
+		}
+
+		if ( is_callable( $args->sanitize_callback ) ) {
+			add_filter( "sanitize_{$object_type}_{$object_subtype}_meta_{$meta_key}", $args->sanitize_callback, 10, 4 );
+		}
+
+		// @todo the only core application of this filter is for the `post` object type. What about users, comments, terms?
+		// The auth here is to edit or add meta, not to view.
+		if ( is_callable( $args->old_auth_callback ) ) {
+			add_filter( "auth_{$object_type}_meta_{$meta_key}", $args->old_auth_callback, 10, 6 );
+		}
+
+		// @todo sort above out before going further
+		if ( is_callable( $args->auth_callback ) ) {
+			add_filter( "auth_{$object_type}_{$object_subtype}_meta_{$meta_key}", $args->auth_callback, 10, 7 );
+		}
+
+	}
+
+	/**
+	 * Retrieve a list of registered meta keys for an object type and subtype.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type    The type of object. Post, comment, user, term.
+	 * @param string $object_subtype A subtype of the object.
+	 *
+	 * @return array List of registered meta keys.
+	 */
+	public function get_registered_meta_keys( $object_type = 'post', $object_subtype = 'all' ) {
+
+		if ( ! property_exists( $this, $object_type ) ) {
+			return array();
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ] ) ) {
+			return array();
+		}
+
+		return $this->{$object_type}[ $object_subtype ];
+
+	}
+
+	/**
+	 * Check if a meta key is registered.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type
+	 * @param string $meta_key
+	 * @param string $object_subtype
+	 *
+	 * @return bool True if the meta key is registered to the object type and subtype. False if not.
+	 */
+	public function registered_meta_key_exists( $object_type, $meta_key, $object_subtype = 'all' ) {
+
+		if ( ! property_exists( $this, $object_type ) ) {
+			return false;
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ] ) ) {
+			return false;
+		}
+
+		if ( ! isset( $this->{$object_type}[ $object_subtype ][ $meta_key ] ) ) {
+			return false;
+		}
+
+		return true;
+
+	}
+
+	/**
+	 * Unregister a meta key from the list of registered keys.
+	 *
+	 * @since 4.6.0
+	 *
+	 * @param string $object_type    The type of object.
+	 * @param string $meta_key       The meta key.
+	 * @param string $object_subtype Optional. The subtype of the object type.
+	 *
+	 * @return bool|WP_Error True if successful. WP_Error if the meta key is invalid.
+	 */
+	public function unregister_meta_key( $object_type, $meta_key, $object_subtype = 'all' ) {
+
+		if ( ! $this->registered_meta_key_exists( $object_type, $meta_key, $object_subtype ) ) {
+			return new WP_Error( 'meta_key_is_not_registered', __( 'Meta key is not registered.' ) );
+		}
+
+		unset( $this->{$object_type}[ $object_subtype ][ $meta_key ] );
+
+		return true;
+
+	}
+
+}
\ No newline at end of file
diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php
index 2ae86d8..a38222d 100644
--- a/src/wp-includes/default-filters.php
+++ b/src/wp-includes/default-filters.php
@@ -477,4 +477,7 @@
 add_filter( 'oembed_dataparse',       'wp_filter_oembed_result',        10, 3 );
 add_filter( 'oembed_response_data',   'get_oembed_response_data_rich',  10, 4 );
 
+// Meta
+add_action( 'plugins_loaded', '_wp_initialize_meta_manager', 0 );
+
 unset( $filter, $action );
diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php
index bfdd595..89063a7 100644
--- a/src/wp-includes/meta.php
+++ b/src/wp-includes/meta.php
@@ -936,14 +936,16 @@ function is_protected_meta( $meta_key, $meta_type = null ) {
  * Sanitize meta value.
  *
  * @since 3.1.3
+ * @since 4.6.0 Added the `$object_subtype` parameter.
+ *
+ * @param string $meta_key       Meta key.
+ * @param mixed  $meta_value     Meta value to sanitize.
+ * @param string $object_type    Type of object the meta is registered to.
+ * @param string $object_subtype Optional. Subtype of object. Will inherit the object type by default.
  *
- * @param string $meta_key   Meta key
- * @param mixed  $meta_value Meta value to sanitize
- * @param string $meta_type  Type of meta
  * @return mixed Sanitized $meta_value
  */
-function sanitize_meta( $meta_key, $meta_value, $meta_type ) {
-
+function sanitize_meta( $meta_key, $meta_value, $object_type, $object_subtype = 'all' ) {
 	/**
 	 * Filter the sanitization of a specific meta key of a specific meta type.
 	 *
@@ -958,30 +960,163 @@ function sanitize_meta( $meta_key, $meta_value, $meta_type ) {
 	 * @param string $meta_key   Meta key.
 	 * @param string $meta_type  Meta type.
 	 */
-	return apply_filters( "sanitize_{$meta_type}_meta_{$meta_key}", $meta_value, $meta_key, $meta_type );
+	$meta_value = apply_filters( "sanitize_{$object_type}_meta_{$meta_key}", $meta_value, $meta_key, $object_type );
+
+	/**
+	 * Filter the sanitization of a specific meta key of a specific meta object type and subtype.
+	 *
+	 * The dynamic portions of the hook name, `$object_type`, `$object_subtype`, and `$meta_key`
+	 * refer to the metadata object type (comment, post, or user), the subtype of that object type
+	 * and the meta key value respectively.
+	 *
+	 * @since 4.5.0
+	 *
+	 * @param mixed  $meta_value     Meta value to sanitize.
+	 * @param string $meta_key       Meta key.
+	 * @param string $object_type    Meta type.
+	 * @param string $object_subtype Subtype of the object type.
+	 */
+	return apply_filters( "sanitize_{$object_type}_{$object_subtype}_meta_{$meta_key}", $meta_value, $meta_key, $object_type, $object_subtype );
 }
 
 /**
- * Register meta key
+ * Register a meta key.
+ *
+ * @todo should we wrap `register_meta()` with `register_meta_key()` to be more explicit?
  *
  * @since 3.3.0
+ * @since 4.6.0 Modified to support an array of data to attach to registered meta keys. Previous arguments for
+ *              `$sanitize_callback` and `$auth_callback` have been folded into this array.
+ *
+ * @param string $object_type    Type of object this meta is registered to.
+ * @param string $meta_key       Meta key to register.
+ * @param array  $args {
+ *     Data used to describe the meta key when registered.
  *
- * @param string       $meta_type         Type of meta
- * @param string       $meta_key          Meta key
- * @param string|array $sanitize_callback A function or method to call when sanitizing the value of $meta_key.
- * @param string|array $auth_callback     Optional. A function or method to call when performing edit_post_meta, add_post_meta, and delete_post_meta capability checks.
+ *     @type string   $sanitize_callback A function or method to call when sanitizing `$meta_key` data.
+ *     @type string   $auth_callback     Optional. A function or method to call when performing edit_post_meta,
+ *                                       add_post_meta, and delete_post_meta capability checks.
+ *     @type bool     $public            Whether data associated with this meta key can be considered public.
+ *     @type array    $schema            The meta field's schema, according to JSON schema.
+ * }
+ * @return true|WP_Error
  */
-function register_meta( $meta_type, $meta_key, $sanitize_callback, $auth_callback = null ) {
-	if ( is_callable( $sanitize_callback ) )
-		add_filter( "sanitize_{$meta_type}_meta_{$meta_key}", $sanitize_callback, 10, 3 );
+function register_meta( $object_type, $meta_key, $args ) {
 
-	if ( empty( $auth_callback ) ) {
-		if ( is_protected_meta( $meta_key, $meta_type ) )
-			$auth_callback = '__return_false';
-		else
-			$auth_callback = '__return_true';
+	global $wp_meta_manager;
+
+	$defaults = array(
+		'object_type' => $object_type,
+		'key'         => $meta_key,
+	);
+
+	$passed_args = func_get_args();
+
+	// Backwards compatibility for sanitize_callback
+	if ( is_callable( $passed_args[2] ) ) {
+		// Handle the old sanitize_callback callable argument
+		$defaults['old_sanitize_callback'] = $passed_args[2];
+
+		// No args passed
+		$args = array();
 	}
 
-	if ( is_callable( $auth_callback ) )
-		add_filter( "auth_{$meta_type}_meta_{$meta_key}", $auth_callback, 10, 6 );
+	// Backwards compatibility for auth_callback
+	if ( isset( $passed_args[3] ) && is_callable( $passed_args[3] ) ) {
+		$defaults['old_auth_callback'] = $passed_args[3];
+	}
+
+	if ( $args && is_array( $args ) ) {
+		// Merge passed $args array with defaults
+		$args = array_merge( $defaults, $args );
+	} else {
+		// No $args passed, use defaults
+		$args = $defaults;
+	}
+
+	return $wp_meta_manager->register( $args );
+
+}
+
+/**
+ * Check if a meta key is registered.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type
+ * @param string $meta_key
+ * @param string $object_subtype
+ *
+ * @return bool True if the meta key is registered to the object type and subtype. False if not.
+ */
+function registered_meta_key_exists( $object_type, $meta_key, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	return $wp_meta_manager->registered_meta_key_exists( $object_type, $meta_key, $object_subtype );
+}
+
+/**
+ * Unregister a meta key from the list of registered keys.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type    The type of object.
+ * @param string $meta_key       The meta key.
+ * @param string $object_subtype Optional. The subtype of the object type.
+ *
+ * @return bool|WP_Error True if successful. WP_Error if the meta key is invalid.
+ */
+function unregister_meta_key( $object_type, $meta_key, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	return $wp_meta_manager->unregister_meta_key( $object_type, $meta_key, $object_subtype );
+}
+
+/**
+ * Retrieve registered metadata for a specified object.
+ *
+ * @since 4.6.0
+ *
+ * @param string $object_type    Type of object to request metadata for. (e.g. comment, post, term, user)
+ * @param int    $object_id      ID of the object the metadata is for.
+ * @param string $meta_key       Optional. Registered metadata key. If not specified, retrieve all registered
+ *                               metadata for the specified object.
+ * @param bool   $single         Optional. If true, return only the first value associated
+ *                               with the specified meta key. If false, return an array of values. Default false.
+ * @param string $object_subtype The subtype of the object's type to request metadata for. (e.g. custom post type)
+ *
+ * @return mixed|WP_Error
+ */
+function get_registered_metadata( $object_type = 'post', $object_id, $meta_key = '', $single = false, $object_subtype = 'all' ) {
+	global $wp_meta_manager;
+	if ( ! empty( $meta_key ) && ! registered_meta_key_exists( $object_type, $meta_key, $object_subtype ) ) {
+		return new WP_Error( 'invalid_meta_key', __( 'Invalid meta key. Not registered.' ) );
+	}
+
+	if ( 'post' === $object_type && $object_subtype !== get_post_type( $object_id ) ) {
+		return new WP_Error( 'invalid_meta_key', __( 'Invalid meta key. Not registered for this subtype.' ) );
+	}
+
+	$data = get_metadata( $object_type, $object_id, $meta_key, $single );
+
+	// If a meta key was specified, return the value associated with that key.
+	if ( ! empty( $meta_key ) ) {
+		return $data;
+	}
+
+	$meta_keys = get_registered_meta_keys( $object_type, $object_subtype );
+    $registered_data = array();
+
+	foreach( $meta_keys as $k => $v ) {
+		if ( isset( $data[ $k ] ) ) {
+			$registered_data[ $k ] = $data[ $k ];
+		}
+	}
+
+	return $registered_data;
+}
+
+
+function _wp_initialize_meta_manager() {
+	if ( empty( $GLOBALS['wp_meta_manager'] ) ) {
+		$GLOBALS['wp_meta_manager'] = new WP_Meta_Manager();
+	}
 }
diff --git a/src/wp-settings.php b/src/wp-settings.php
index a4bb370..768362a 100644
--- a/src/wp-settings.php
+++ b/src/wp-settings.php
@@ -133,6 +133,7 @@
 require( ABSPATH . WPINC . '/user.php' );
 require( ABSPATH . WPINC . '/class-wp-user-query.php' );
 require( ABSPATH . WPINC . '/session.php' );
+require( ABSPATH . WPINC . '/class-wp-meta-manager.php' );
 require( ABSPATH . WPINC . '/meta.php' );
 require( ABSPATH . WPINC . '/class-wp-meta-query.php' );
 require( ABSPATH . WPINC . '/class-wp-metadata-lazyloader.php' );
diff --git a/tests/phpunit/tests/meta/registerMeta.php b/tests/phpunit/tests/meta/registerMeta.php
new file mode 100644
index 0000000..1601041
--- /dev/null
+++ b/tests/phpunit/tests/meta/registerMeta.php
@@ -0,0 +1,79 @@
+<?php
+
+/**
+ * @group meta
+ */
+class Tests_Meta_Register_Meta extends WP_UnitTestCase {
+	protected static $editor_id;
+	protected static $post_id;
+	protected static $comment_id;
+
+	public static function wpSetUpBeforeClass( $factory ) {
+		self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) );
+		self::$post_id = $factory->post->create();
+		self::$comment_id = $factory->comment->create( array( 'comment_post_ID' => self::$post_id ) );
+	}
+
+	public static function wpTearDownAfterClass() {
+		self::delete_user( self::$editor_id );
+		wp_delete_comment( self::$comment_id, true );
+		wp_delete_post( self::$post_id, true );
+	}
+
+	function setUp() {
+		parent::setUp();
+		wp_set_current_user( self::$editor_id );
+	}
+
+	public function _old_sanitize_meta_cb( $meta_value, $meta_key, $meta_type ) {
+		return $meta_key . ' sanitized';
+	}
+
+	public function _new_sanitize_meta_cb( $meta_value, $meta_key, $object_type, $object_subtype ) {
+		return $meta_key . ' sanitized';
+	}
+
+	public function test_register_meta_without_sanitize_callback_registers_meta_key() {
+		register_meta( 'post', 'flight_number', array() );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'flight_number' ) );
+	}
+
+	public function test_register_meta_for_a_category() {
+		register_meta( 'taxonomy', 'category_icon', array( 'object_subtype' => 'category' ) );
+		$this->assertTrue( registered_meta_key_exists( 'taxonomy', 'category_icon', 'category' ) );
+	}
+
+	public function test_register_meta_for_a_comment() {
+		register_meta( 'comment', 'comment_rating', array( 'object_subtype' => 'comment' ) );
+		$this->assertTrue( registered_meta_key_exists( 'comment', 'comment_rating', 'comment' ) );
+	}
+
+	public function test_register_meta_with_sanitize_callback_registers_meta_key() {
+		register_meta( 'post', 'flight_number', array( 'sanitize_callback' => 'esc_html' ) );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'flight_number' ) );
+	}
+
+	public function test_register_meta_valid_object_type_with_valid_subtype() {
+		register_meta( 'post', 'longitude', array( 'object_subtype' => 'post' ) );
+		$this->assertTrue( registered_meta_key_exists( 'post', 'longitude', 'post' ) );
+	}
+
+	public function test_register_meta_valid_object_type_with_invalid_subtype() {
+		register_meta( 'post', 'latitude', array( 'object_subtype' => 'invalid-type' ) );
+		$this->assertFalse( registered_meta_key_exists( 'post', 'latitude', 'invalid-type' ) );
+	}
+
+	public function test_register_meta_with_old_sanitize_callback_parameter() {
+		register_meta( 'post', 'old_sanitized_key', array( $this, '_old_sanitize_meta_cb' ) );
+		$meta = sanitize_meta( 'old_sanitized_key', 'unsanitized', 'post' );
+
+		$this->assertEquals( 'old_sanitized_key sanitized', $meta );
+	}
+
+	public function test_register_meta_with_new_sanitize_callback_parameter() {
+		register_meta( 'post', 'new_sanitized_key', array( 'sanitize_callback' => array( $this, '_new_sanitize_meta_cb' ) ) );
+		$meta = sanitize_meta( 'new_sanitized_key', 'unsanitized', 'post' );
+
+		$this->assertEquals( 'new_sanitized_key sanitized', $meta );
+	}
+}
