diff --git a/tensorflow/lite/delegates/gpu/metal/BUILD b/tensorflow/lite/delegates/gpu/metal/BUILD
index b2fc7e2d4bf..335a9ba167b 100644
--- a/tensorflow/lite/delegates/gpu/metal/BUILD
+++ b/tensorflow/lite/delegates/gpu/metal/BUILD
@@ -73,7 +73,7 @@ objc_library(
         "Metal",
     ],
     deps = [
-        "//tensorflow/lite/delegates/gpu/common:gpu_info",
+        "//tensorflow/lite/delegates/gpu/common:data_type",
         "//tensorflow/lite/delegates/gpu/common:status",
     ],
 )
@@ -196,6 +196,7 @@ objc_library(
         ":buffer",
         ":gpu_object",
         ":metal_spatial_tensor",
+        ":texture2d",
         "//tensorflow/lite/delegates/gpu/common:status",
         "//tensorflow/lite/delegates/gpu/common:util",
         "//tensorflow/lite/delegates/gpu/common/task:arguments",
@@ -250,6 +251,38 @@ objc_library(
     ],
 )
 
+objc_library(
+    name = "texture2d",
+    srcs = ["texture2d.cc"],
+    hdrs = ["texture2d.h"],
+    copts = DEFAULT_COPTS + [
+        "-ObjC++",
+    ],
+    sdk_frameworks = ["Metal"],
+    deps = [
+        ":common",
+        ":gpu_object",
+        "//tensorflow/lite/delegates/gpu/common:data_type",
+        "//tensorflow/lite/delegates/gpu/common:status",
+        "//tensorflow/lite/delegates/gpu/common/task:texture2d_desc",
+        "@com_google_absl//absl/types:span",
+    ],
+)
+
+objc_library(
+    name = "texture2d_test_lib",
+    testonly = 1,
+    srcs = ["texture2d_test.mm"],
+    sdk_frameworks = [
+        "XCTest",
+        "Metal",
+    ],
+    deps = [
+        ":texture2d",
+        "//tensorflow/lite/delegates/gpu/common:types",
+    ],
+)
+
 objc_library(
     name = "TestBinary",
     testonly = 1,
@@ -282,6 +315,7 @@ objc_library(
         "//tensorflow/lite/delegates/gpu/metal:buffer_test.mm",
         "//tensorflow/lite/delegates/gpu/metal:common_test.mm",
         "//tensorflow/lite/delegates/gpu/metal:metal_spatial_tensor_test.mm",
+        "//tensorflow/lite/delegates/gpu/metal:texture2d_test.mm",
     ],
     hdrs = [
     ],
@@ -293,6 +327,7 @@ objc_library(
         "//tensorflow/lite/delegates/gpu/metal:common",
         "//tensorflow/lite/delegates/gpu/metal:inference_context",
         "//tensorflow/lite/delegates/gpu/metal:metal_spatial_tensor",
+        "//tensorflow/lite/delegates/gpu/metal:texture2d",
         "//tensorflow/lite/delegates/gpu/metal/kernels:test_util",
         "@com_google_absl//absl/memory",
     ],
diff --git a/tensorflow/lite/delegates/gpu/metal/common.h b/tensorflow/lite/delegates/gpu/metal/common.h
index 6f4e94ed2e7..8392b475603 100644
--- a/tensorflow/lite/delegates/gpu/metal/common.h
+++ b/tensorflow/lite/delegates/gpu/metal/common.h
@@ -20,6 +20,7 @@ limitations under the License.
 
 #include <utility>
 
+#include "tensorflow/lite/delegates/gpu/common/data_type.h"
 #include "tensorflow/lite/delegates/gpu/common/status.h"
 
 namespace tflite {
@@ -43,6 +44,9 @@ absl::Status CreateComputeProgram(id<MTLDevice> device, NSString* code, NSString
                                   NSDictionary<NSString*, NSString*>* macros,
                                   id<MTLComputePipelineState>* program);
 
+int PixelFormatToSizeInBytes(MTLPixelFormat pixel_format);
+MTLPixelFormat DataTypeToRGBAPixelFormat(DataType type, bool normalized = false);
+
 }  // namespace metal
 }  // namespace gpu
 }  // namespace tflite
diff --git a/tensorflow/lite/delegates/gpu/metal/common.mm b/tensorflow/lite/delegates/gpu/metal/common.mm
index cc5a98dfffc..a7732689526 100644
--- a/tensorflow/lite/delegates/gpu/metal/common.mm
+++ b/tensorflow/lite/delegates/gpu/metal/common.mm
@@ -90,6 +90,49 @@ absl::Status CreateComputeProgram(id<MTLDevice> device, NSString* code, NSString
   return absl::OkStatus();
 }
 
+int PixelFormatToSizeInBytes(MTLPixelFormat pixel_format) {
+  if (pixel_format == MTLPixelFormatRGBA32Uint ||
+      pixel_format == MTLPixelFormatRGBA32Sint ||
+      pixel_format == MTLPixelFormatRGBA32Float) {
+    return 16;
+  } else if (pixel_format == MTLPixelFormatRGBA16Unorm ||
+             pixel_format == MTLPixelFormatRGBA16Snorm ||
+             pixel_format == MTLPixelFormatRGBA16Uint ||
+             pixel_format == MTLPixelFormatRGBA16Sint ||
+             pixel_format == MTLPixelFormatRGBA16Float) {
+    return 8;
+  } else if (pixel_format == MTLPixelFormatRGBA8Unorm ||
+             pixel_format == MTLPixelFormatRGBA8Snorm ||
+             pixel_format == MTLPixelFormatRGBA8Uint ||
+             pixel_format == MTLPixelFormatRGBA8Sint) {
+    return 4;
+  }
+  return -1;
+}
+
+MTLPixelFormat DataTypeToRGBAPixelFormat(DataType type, bool normalized) {
+  switch (type) {
+    case DataType::FLOAT32:
+      return MTLPixelFormatRGBA32Float;
+    case DataType::FLOAT16:
+      return MTLPixelFormatRGBA16Float;
+    case DataType::INT8:
+      return normalized ? MTLPixelFormatRGBA8Snorm : MTLPixelFormatRGBA8Sint;
+    case DataType::UINT8:
+      return normalized ? MTLPixelFormatRGBA8Unorm : MTLPixelFormatRGBA8Uint;
+    case DataType::INT16:
+      return normalized ? MTLPixelFormatRGBA16Snorm : MTLPixelFormatRGBA16Sint;
+    case DataType::UINT16:
+      return normalized ? MTLPixelFormatRGBA16Unorm : MTLPixelFormatRGBA16Uint;
+    case DataType::INT32:
+      return MTLPixelFormatRGBA32Sint;
+    case DataType::UINT32:
+      return MTLPixelFormatRGBA32Uint;
+    default:
+      return MTLPixelFormatInvalid;
+  }
+}
+
 }  // namespace metal
 }  // namespace gpu
 }  // namespace tflite
diff --git a/tensorflow/lite/delegates/gpu/metal/metal_arguments.cc b/tensorflow/lite/delegates/gpu/metal/metal_arguments.cc
index 68d73f70c02..9da54e14a51 100644
--- a/tensorflow/lite/delegates/gpu/metal/metal_arguments.cc
+++ b/tensorflow/lite/delegates/gpu/metal/metal_arguments.cc
@@ -21,6 +21,7 @@ limitations under the License.
 #include "tensorflow/lite/delegates/gpu/common/util.h"
 #include "tensorflow/lite/delegates/gpu/metal/buffer.h"
 #include "tensorflow/lite/delegates/gpu/metal/metal_spatial_tensor.h"
+#include "tensorflow/lite/delegates/gpu/metal/texture2d.h"
 
 namespace tflite {
 namespace gpu {
@@ -129,6 +130,15 @@ absl::Status CreateMetalObject(id<MTLDevice> device, GPUObjectDescriptor* desc,
     return absl::OkStatus();
   }
 
+  const auto* texture_desc = dynamic_cast<const Texture2DDescriptor*>(desc);
+  if (texture_desc) {
+    Texture2D gpu_texture;
+    RETURN_IF_ERROR(
+        gpu_texture.CreateFromTexture2DDescriptor(*texture_desc, device));
+    *result = absl::make_unique<Texture2D>(std::move(gpu_texture));
+    return absl::OkStatus();
+  }
+
   const auto* tensor_desc = dynamic_cast<const TensorDescriptor*>(desc);
   if (tensor_desc) {
     MetalSpatialTensor gpu_tensor;
diff --git a/tensorflow/lite/delegates/gpu/metal/texture2d.cc b/tensorflow/lite/delegates/gpu/metal/texture2d.cc
new file mode 100644
index 00000000000..b0076dd78d5
--- /dev/null
+++ b/tensorflow/lite/delegates/gpu/metal/texture2d.cc
@@ -0,0 +1,179 @@
+/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#include "tensorflow/lite/delegates/gpu/metal/texture2d.h"
+
+namespace tflite {
+namespace gpu {
+namespace metal {
+namespace {
+
+// Creates new 4-channel 2D texture with cl_channel_type elements
+absl::Status CreateTexture2D(int width, int height, DataType type, void* data,
+                             id<MTLDevice> device, Texture2D* result) {
+  MTLPixelFormat pixel_format = DataTypeToRGBAPixelFormat(type);
+
+  MTLTextureDescriptor* texture_desc =
+      [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixel_format
+                                                         width:width
+                                                        height:height
+                                                     mipmapped:NO];
+  texture_desc.textureType = MTLTextureType2D;
+  texture_desc.usage = MTLTextureUsageShaderRead;
+  texture_desc.storageMode = MTLStorageModePrivate;
+
+  id<MTLTexture> texture = [device newTextureWithDescriptor:texture_desc];
+  if (!texture) {
+    return absl::UnknownError("Failed to allocate id<MTLTexture>");
+  }
+
+  if (data) {
+    MTLRegion region = {
+        {0, 0, 0},
+        {static_cast<NSUInteger>(width), static_cast<NSUInteger>(height), 1}};
+    const int pixel_size = PixelFormatToSizeInBytes(pixel_format);
+    [texture replaceRegion:region
+               mipmapLevel:0
+                 withBytes:data
+               bytesPerRow:width * pixel_size];
+
+    if (!texture) {
+      return absl::UnknownError("Failed to upload data to id<MTLTexture>");
+    }
+  }
+
+  *result = Texture2D(texture, width, height, pixel_format);
+
+  return absl::OkStatus();
+}
+}  // namespace
+
+Texture2D::Texture2D(id<MTLTexture> texture, int width, int height,
+                     MTLPixelFormat pixel_format)
+    : texture_(texture),
+      width_(width),
+      height_(height),
+      pixel_format_(pixel_format) {}
+
+Texture2D::Texture2D(Texture2D&& texture)
+    : texture_(texture.texture_),
+      width_(texture.width_),
+      height_(texture.height_),
+      pixel_format_(texture.pixel_format_) {
+  texture.texture_ = nullptr;
+  texture.width_ = 0;
+  texture.height_ = 0;
+}
+
+Texture2D& Texture2D::operator=(Texture2D&& texture) {
+  if (this != &texture) {
+    Release();
+    std::swap(pixel_format_, texture.pixel_format_);
+    std::swap(width_, texture.width_);
+    std::swap(height_, texture.height_);
+    std::swap(texture_, texture.texture_);
+  }
+  return *this;
+}
+
+void Texture2D::Release() {
+  if (texture_) {
+    texture_ = nullptr;
+    width_ = 0;
+    height_ = 0;
+  }
+}
+
+absl::Status Texture2D::GetGPUResources(
+    const GPUObjectDescriptor* obj_ptr,
+    GPUResourcesWithValue* resources) const {
+  const auto* texture_desc = dynamic_cast<const Texture2DDescriptor*>(obj_ptr);
+  if (!texture_desc) {
+    return absl::InvalidArgumentError("Expected Texture2DDescriptor on input.");
+  }
+
+  resources->images2d.push_back({"tex2d", texture_});
+  return absl::OkStatus();
+}
+
+absl::Status Texture2D::CreateFromTexture2DDescriptor(
+    const Texture2DDescriptor& desc, id<MTLDevice> device) {
+  width_ = desc.size.x;
+  height_ = desc.size.y;
+  pixel_format_ = DataTypeToRGBAPixelFormat(desc.element_type, desc.normalized);
+  uint8_t* data_ptr = desc.data.empty()
+                          ? nullptr
+                          : const_cast<unsigned char*>(desc.data.data());
+
+  MTLTextureDescriptor* texture_desc =
+      [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixel_format_
+                                                         width:width_
+                                                        height:height_
+                                                     mipmapped:NO];
+  texture_desc.textureType = MTLTextureType2D;
+  texture_desc.usage = MTLTextureUsageShaderRead;
+  texture_desc.storageMode = MTLStorageModePrivate;
+
+  texture_ = [device newTextureWithDescriptor:texture_desc];
+  if (!texture_) {
+    return absl::UnknownError("Failed to allocate id<MTLTexture>");
+  }
+
+  if (data_ptr) {
+    MTLRegion region = {
+        {0, 0, 0},
+        {static_cast<NSUInteger>(width_), static_cast<NSUInteger>(height_), 1}};
+    const int pixel_size = PixelFormatToSizeInBytes(pixel_format_);
+    [texture_ replaceRegion:region
+                mipmapLevel:0
+                  withBytes:data_ptr
+                bytesPerRow:width_ * pixel_size];
+
+    if (!texture_) {
+      return absl::UnknownError("Failed to upload data to id<MTLTexture>");
+    }
+  }
+
+  return absl::OkStatus();
+}
+
+// Creates new 4-channel 2D texture with f32 elements
+absl::Status CreateTexture2DRGBA32F(int width, int height, id<MTLDevice> device,
+                                    Texture2D* result) {
+  return CreateTexture2D(width, height, DataType::FLOAT32, nullptr, device,
+                         result);
+}
+
+// Creates new 4-channel 2D texture with f16 elements
+absl::Status CreateTexture2DRGBA16F(int width, int height, id<MTLDevice> device,
+                                    Texture2D* result) {
+  return CreateTexture2D(width, height, DataType::FLOAT16, nullptr, device,
+                         result);
+}
+
+absl::Status CreateTexture2DRGBA(DataType type, int width, int height,
+                                 id<MTLDevice> device, Texture2D* result) {
+  return CreateTexture2D(width, height, type, nullptr, device, result);
+}
+
+absl::Status CreateTexture2DRGBA(DataType type, int width, int height,
+                                 void* data, id<MTLDevice> device,
+                                 Texture2D* result) {
+  return CreateTexture2D(width, height, type, data, device, result);
+}
+
+}  // namespace metal
+}  // namespace gpu
+}  // namespace tflite
diff --git a/tensorflow/lite/delegates/gpu/metal/texture2d.h b/tensorflow/lite/delegates/gpu/metal/texture2d.h
new file mode 100644
index 00000000000..5051caadc42
--- /dev/null
+++ b/tensorflow/lite/delegates/gpu/metal/texture2d.h
@@ -0,0 +1,122 @@
+/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#ifndef TENSORFLOW_LITE_DELEGATES_GPU_METAL_TEXTURE2D_H_
+#define TENSORFLOW_LITE_DELEGATES_GPU_METAL_TEXTURE2D_H_
+
+#import <Metal/Metal.h>
+
+#include "absl/types/span.h"
+#include "tensorflow/lite/delegates/gpu/common/data_type.h"
+#include "tensorflow/lite/delegates/gpu/common/status.h"
+#include "tensorflow/lite/delegates/gpu/common/task/texture2d_desc.h"
+#include "tensorflow/lite/delegates/gpu/metal/common.h"
+#include "tensorflow/lite/delegates/gpu/metal/gpu_object.h"
+
+namespace tflite {
+namespace gpu {
+namespace metal {
+
+// Texture2D represent formatted GPU data storage.
+// Texture2D is moveable but not copyable.
+class Texture2D : public GPUObject {
+ public:
+  Texture2D() {}  // just for using Texture2D as a class members
+  Texture2D(id<MTLTexture> texture, int width, int height, MTLPixelFormat pixel_format);
+
+  // Move only
+  Texture2D(Texture2D&& texture);
+  Texture2D& operator=(Texture2D&& texture);
+  Texture2D(const Texture2D&) = delete;
+  Texture2D& operator=(const Texture2D&) = delete;
+
+  ~Texture2D() override { Release(); }
+
+  // Writes data to a texture. Data should point to a region that
+  // has exact width * height * sizeof(pixel) bytes.
+  template <typename T>
+  absl::Status WriteData(const absl::Span<T> data);
+
+  // Reads data from Texture2D into CPU memory.
+  template <typename T>
+  absl::Status ReadData(std::vector<T>* result) const;
+
+  absl::Status GetGPUResources(const GPUObjectDescriptor* obj_ptr,
+                               GPUResourcesWithValue* resources) const override;
+
+  absl::Status CreateFromTexture2DDescriptor(const Texture2DDescriptor& desc, id<MTLDevice> device);
+
+ private:
+  void Release();
+
+  id<MTLTexture> texture_ = nullptr;
+  int width_;
+  int height_;
+  MTLPixelFormat pixel_format_;
+};
+
+// Creates new 4-channel 2D texture with f32 elements
+absl::Status CreateTexture2DRGBA32F(int width, int height, id<MTLDevice> device, Texture2D* result);
+
+// Creates new 4-channel 2D texture with f16 elements
+absl::Status CreateTexture2DRGBA16F(int width, int height, id<MTLDevice> device, Texture2D* result);
+
+absl::Status CreateTexture2DRGBA(DataType type, int width, int height, id<MTLDevice> device,
+                                 Texture2D* result);
+
+absl::Status CreateTexture2DRGBA(DataType type, int width, int height, void* data,
+                                 id<MTLDevice> device, Texture2D* result);
+
+template <typename T>
+absl::Status Texture2D::WriteData(const absl::Span<T> data) {
+  const int pixel_size = PixelFormatToSizeInBytes(pixel_format_);
+  if (width_ * height_ * pixel_size != data.size() * sizeof(T)) {
+    return absl::InvalidArgumentError(
+        "absl::Span<T> data size is different from texture allocated size.");
+  }
+
+  MTLRegion region = {{0, 0, 0},
+                      {static_cast<NSUInteger>(width_), static_cast<NSUInteger>(height_), 1}};
+  [texture_ replaceRegion:region
+              mipmapLevel:0
+                withBytes:data.data()
+              bytesPerRow:width_ * pixel_size];
+
+  return absl::OkStatus();
+}
+
+template <typename T>
+absl::Status Texture2D::ReadData(std::vector<T>* result) const {
+  const int pixel_size = PixelFormatToSizeInBytes(pixel_format_);
+  if (pixel_size % sizeof(T) != 0) {
+    return absl::InvalidArgumentError("Pixel format is different.");
+  }
+  result->resize(width_ * height_ * (pixel_size / sizeof(T)));
+
+  MTLRegion region = {{0, 0, 0},
+                      {static_cast<NSUInteger>(width_), static_cast<NSUInteger>(height_), 1}};
+  [texture_ getBytes:result->data()
+         bytesPerRow:width_ * pixel_size
+          fromRegion:region
+         mipmapLevel:0];
+
+  return absl::OkStatus();
+}
+
+}  // namespace metal
+}  // namespace gpu
+}  // namespace tflite
+
+#endif  // TENSORFLOW_LITE_DELEGATES_GPU_METAL_TEXTURE2D_H_
diff --git a/tensorflow/lite/delegates/gpu/metal/texture2d_test.mm b/tensorflow/lite/delegates/gpu/metal/texture2d_test.mm
new file mode 100644
index 00000000000..fc722c4cbc8
--- /dev/null
+++ b/tensorflow/lite/delegates/gpu/metal/texture2d_test.mm
@@ -0,0 +1,70 @@
+/* Copyright 2021 The TensorFlow Authors. All Rights Reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+==============================================================================*/
+
+#include "tensorflow/lite/delegates/gpu/metal/texture2d.h"
+
+#import <Metal/Metal.h>
+#import <XCTest/XCTest.h>
+
+#include <iostream>
+#include <vector>
+
+#include "tensorflow/lite/delegates/gpu/common/types.h"
+
+@interface Texture2DTest : XCTestCase
+@end
+
+@implementation Texture2DTest
+- (void)setUp {
+  [super setUp];
+}
+
+using tflite::gpu::half;
+
+- (void)testTexture2DF32 {
+  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+
+  const std::vector<float> data = {1.0, 2.0, 3.0, -4.0, 5.1, 6.7, 4.1, 6.17};
+  tflite::gpu::metal::Texture2D texture;
+  XCTAssertTrue(tflite::gpu::metal::CreateTexture2DRGBA32F(1, 2, device, &texture).ok());
+  XCTAssertTrue(texture.WriteData(absl::MakeConstSpan(data.data(), data.size())).ok());
+  std::vector<float> gpu_data;
+  XCTAssertTrue(texture.ReadData<float>(&gpu_data).ok());
+
+  XCTAssertEqual(gpu_data.size(), data.size());
+  for (int i = 0; i < gpu_data.size(); ++i) {
+    XCTAssertEqual(gpu_data[i], data[i]);
+  }
+}
+
+- (void)testTexture2DF16 {
+  id<MTLDevice> device = MTLCreateSystemDefaultDevice();
+
+  const std::vector<half> data = {half(1.4),  half(2.1),  half(2.2), half(1.34),
+                                  half(20.1), half(2.24), half(0.1), half(0.2)};
+
+  tflite::gpu::metal::Texture2D texture;
+  XCTAssertTrue(tflite::gpu::metal::CreateTexture2DRGBA16F(2, 1, device, &texture).ok());
+  XCTAssertTrue(texture.WriteData(absl::MakeConstSpan(data.data(), data.size())).ok());
+  std::vector<half> gpu_data;
+  XCTAssertTrue(texture.ReadData<half>(&gpu_data).ok());
+
+  XCTAssertEqual(gpu_data.size(), data.size());
+  for (int i = 0; i < gpu_data.size(); ++i) {
+    XCTAssertEqual(gpu_data[i], data[i]);
+  }
+}
+
+@end